mirror of
https://github.com/httprunner/httprunner.git
synced 2026-05-13 08:59:44 +08:00
@@ -118,6 +118,12 @@ Use "hrp [command] --help" for more information about a command.
|
||||
<a href="https://httprunner.com/docs/cases/umcare"><img src="https://httprunner.com/image/logo/umcare.png" title="通用环球医疗 - 使用 HttpRunner 实践接口自动化测试" width="100"></a>
|
||||
<a href="https://httprunner.com/docs/cases/mihoyo"><img src="https://httprunner.com/image/logo/miHoYo.png" title="米哈游 - 基于 HttpRunner 搭建接口自动化测试体系" width="100"></a>
|
||||
|
||||
## Sponsor
|
||||
|
||||
[<img src="https://testing-studio.com/img/icon.png" alt="霍格沃兹测试开发学社" width="500">](https://qrcode.testing-studio.com/f?from=HttpRunner&url=https://testing-studio.com/)
|
||||
|
||||
> 霍格沃兹测试开发学社是中国软件测试开发高端教育品牌,产品由国内顶尖软件测试开发技术专家携手打造,为企业与个人提供专业的技能培训与咨询、测试工具与测试平台、测试外包与测试众包服务。领域涵盖 App/Web 自动化测试、接口自动化测试、性能测试、安全测试、持续交付/DevOps、测试左移、测试右移、精准测试、测试平台开发、测试管理等方向。-> [**联系我们**](http://qrcode.testing-studio.com/f?from=HttpRunner&url=https://ceshiren.com/t/topic/23745)
|
||||
|
||||
## Subscribe
|
||||
|
||||
关注 HttpRunner 的微信公众号,第一时间获得最新资讯。
|
||||
|
||||
@@ -112,6 +112,12 @@ Use "hrp [command] --help" for more information about a command.
|
||||
<a href="https://httprunner.com/docs/cases/mihoyo"><img src="https://httprunner.com/image/logo/miHoYo.png" title="米哈游 - 基于 HttpRunner 搭建接口自动化测试体系" width="100"></a>
|
||||
|
||||
|
||||
## 赞助商
|
||||
|
||||
[<img src="https://testing-studio.com/img/icon.png" alt="霍格沃兹测试开发学社" width="500">](https://qrcode.testing-studio.com/f?from=HttpRunner&url=https://testing-studio.com/)
|
||||
|
||||
> 霍格沃兹测试开发学社是中国软件测试开发高端教育品牌,产品由国内顶尖软件测试开发技术专家携手打造,为企业与个人提供专业的技能培训与咨询、测试工具与测试平台、测试外包与测试众包服务。领域涵盖 App/Web 自动化测试、接口自动化测试、性能测试、安全测试、持续交付/DevOps、测试左移、测试右移、精准测试、测试平台开发、测试管理等方向。-> [**联系我们**](http://qrcode.testing-studio.com/f?from=HttpRunner&url=https://ceshiren.com/t/topic/23745)
|
||||
|
||||
## Subscribe
|
||||
|
||||
关注 HttpRunner 的微信公众号,第一时间获得最新资讯。
|
||||
|
||||
@@ -1,5 +1,26 @@
|
||||
# Release History
|
||||
|
||||
## v4.3.3 (2023-04-19)
|
||||
|
||||
**go version**
|
||||
|
||||
- feat: add `sleep_random` to sleep random seconds, with weight for multiple time ranges
|
||||
- feat: input text with adb
|
||||
- feat: add adb `screencap` sub command
|
||||
- feat: add `IsAppInForeground` to check if the given package is in foreground
|
||||
- feat: check if app is in foreground when step failed
|
||||
- feat: add validator AssertAppInForeground and AssertAppNotInForeground
|
||||
- feat: save screenshots of all steps including ocr and cv recognition process data
|
||||
- fix: adb driver for TapFloat
|
||||
- fix: stop logcat only when enabled
|
||||
- fix: do not fail case when kill logcat error
|
||||
- fix: take screenshot after each step
|
||||
- fix: screencap compatibility for shell v1 and v2 protocol
|
||||
- fix: display parsed url in html report
|
||||
- fix: fast fail not closing the websocket connection
|
||||
- fix #1467: failed to parse parameters with plugin functions
|
||||
- fix #1549: avoid duplicate creating plugins
|
||||
|
||||
## v4.3.2 (2022-12-26)
|
||||
|
||||
**go version**
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 75 KiB |
BIN
docs/assets/hogwarts.png
Normal file
BIN
docs/assets/hogwarts.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 155 KiB |
@@ -1,4 +1,4 @@
|
||||
# NOTE: Generated By HttpRunner v4.1.4
|
||||
# NOTE: Generated By HttpRunner v4.3.0
|
||||
# FROM: a-b.c/1.yml
|
||||
from httprunner import HttpRunner, Config, Step, RunRequest
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# NOTE: Generated By HttpRunner v4.1.4
|
||||
# NOTE: Generated By HttpRunner v4.3.0
|
||||
# FROM: a-b.c/2 3.yml
|
||||
from httprunner import HttpRunner, Config, Step, RunRequest
|
||||
from httprunner import RunTestCase
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
config:
|
||||
name: basic test with httpbin
|
||||
base_url: http://httpbin.org/
|
||||
base_url: https://httpbin.org/
|
||||
|
||||
teststeps:
|
||||
-
|
||||
|
||||
@@ -5,7 +5,7 @@ from httprunner import HttpRunner, Config, Step, RunRequest
|
||||
|
||||
class TestCaseValidate(HttpRunner):
|
||||
|
||||
config = Config("basic test with httpbin").base_url("http://httpbin.org/")
|
||||
config = Config("basic test with httpbin").base_url("https://httpbin.org/")
|
||||
|
||||
teststeps = [
|
||||
Step(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# NOTE: Generated By HttpRunner v4.1.4
|
||||
# NOTE: Generated By HttpRunner v4.3.0
|
||||
# FROM: request_methods/hardcode.yml
|
||||
from httprunner import HttpRunner, Config, Step, RunRequest
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# NOTE: Generated By HttpRunner v4.1.4
|
||||
# NOTE: Generated By HttpRunner v4.3.0
|
||||
# FROM: request_methods/request_with_functions.yml
|
||||
from httprunner import HttpRunner, Config, Step, RunRequest
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# NOTE: Generated By HttpRunner v4.1.4
|
||||
# NOTE: Generated By HttpRunner v4.3.0
|
||||
# FROM: request_methods/request_with_parameters.yml
|
||||
import pytest
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# NOTE: Generated By HttpRunner v4.1.4
|
||||
# NOTE: Generated By HttpRunner v4.3.0
|
||||
# FROM: request_methods/request_with_testcase_reference.yml
|
||||
from httprunner import HttpRunner, Config, Step, RunRequest
|
||||
from httprunner import RunTestCase
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# NOTE: Generated By HttpRunner v4.1.4
|
||||
# NOTE: Generated By HttpRunner v4.3.0
|
||||
# FROM: request_methods/request_with_variables.yml
|
||||
from httprunner import HttpRunner, Config, Step, RunRequest
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# NOTE: Generated By HttpRunner v4.1.4
|
||||
# NOTE: Generated By HttpRunner v4.3.0
|
||||
# FROM: request_methods/validate_with_functions.yml
|
||||
from httprunner import HttpRunner, Config, Step, RunRequest
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# NOTE: Generated By HttpRunner v4.1.4
|
||||
# NOTE: Generated By HttpRunner v4.3.0
|
||||
# FROM: request_methods/validate_with_variables.yml
|
||||
from httprunner import HttpRunner, Config, Step, RunRequest
|
||||
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
//go:build localtest
|
||||
|
||||
package uitest
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/httprunner/httprunner/v4/hrp"
|
||||
"github.com/httprunner/httprunner/v4/hrp/pkg/uixt"
|
||||
)
|
||||
|
||||
func TestAndroidDouYinLive(t *testing.T) {
|
||||
testCase := &hrp.TestCase{
|
||||
Config: hrp.NewConfig("通过 feed 头像进入抖音直播间").
|
||||
SetAndroid(uixt.WithUIA2(false), uixt.WithAdbLogOn(true)),
|
||||
TestSteps: []hrp.IStep{
|
||||
hrp.NewStep("启动抖音").
|
||||
Android().
|
||||
Home().
|
||||
AppTerminate("com.ss.android.ugc.aweme"). // 关闭已运行的抖音,确保启动抖音后在「抖音」首页
|
||||
SwipeToTapApp("抖音", uixt.WithMaxRetryTimes(5)).
|
||||
Sleep(10),
|
||||
hrp.NewStep("处理青少年弹窗").
|
||||
Android().
|
||||
Tap("推荐").
|
||||
TapByOCR("我知道了", uixt.WithIgnoreNotFoundError(true)).
|
||||
Validate().
|
||||
AssertOCRExists("首页", "抖音启动失败,「首页」不存在"),
|
||||
hrp.NewStep("在推荐页上划,直到出现 feed 头像「直播」").
|
||||
Android().
|
||||
SwipeToTapText("直播", uixt.WithMaxRetryTimes(10), uixt.WithIdentifier("进入直播间")),
|
||||
hrp.NewStep("向上滑动,等待 10s").
|
||||
Android().
|
||||
SwipeUp(uixt.WithIdentifier("第一次上划")).Sleep(10).ScreenShot(). // 上划 1 次,等待 10s,截图保存
|
||||
SwipeUp(uixt.WithIdentifier("第二次上划")).Sleep(10).ScreenShot(), // 再上划 1 次,等待 10s,截图保存
|
||||
},
|
||||
}
|
||||
|
||||
runner := hrp.NewRunner(t).SetSaveTests(true)
|
||||
err := runner.Run(testCase)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
134
examples/uitest/demo_android_feed_swipe.json
Normal file
134
examples/uitest/demo_android_feed_swipe.json
Normal file
@@ -0,0 +1,134 @@
|
||||
{
|
||||
"config": {
|
||||
"name": "点播_抖音_滑动场景_随机间隔_android",
|
||||
"variables": {
|
||||
"device": "${ENV(SerialNumber)}"
|
||||
},
|
||||
"android": [
|
||||
{
|
||||
"serial": "$device"
|
||||
}
|
||||
]
|
||||
},
|
||||
"teststeps": [
|
||||
{
|
||||
"name": "启动抖音",
|
||||
"android": {
|
||||
"actions": [
|
||||
{
|
||||
"method": "app_terminate",
|
||||
"params": "com.ss.android.ugc.aweme"
|
||||
},
|
||||
{
|
||||
"method": "app_launch",
|
||||
"params": "com.ss.android.ugc.aweme"
|
||||
},
|
||||
{
|
||||
"method": "sleep",
|
||||
"params": 10
|
||||
}
|
||||
]
|
||||
},
|
||||
"validate": [
|
||||
{
|
||||
"check": "ui_foreground_app",
|
||||
"assert": "equal",
|
||||
"expect": "com.ss.android.ugc.aweme",
|
||||
"msg": "app [com.ss.android.ugc.aweme] should be in foreground"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "处理青少年弹窗",
|
||||
"android": {
|
||||
"actions": [
|
||||
{
|
||||
"method": "tap_ocr",
|
||||
"params": "我知道了",
|
||||
"ignore_NotFoundError": true
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "滑动 Feed 3 次,随机间隔 0-5s",
|
||||
"android": {
|
||||
"actions": [
|
||||
{
|
||||
"method": "swipe",
|
||||
"params": "up"
|
||||
},
|
||||
{
|
||||
"method": "sleep_random",
|
||||
"params": [
|
||||
0,
|
||||
5
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"loops": 3
|
||||
},
|
||||
{
|
||||
"name": "滑动 Feed 1 次,随机间隔 5-10s",
|
||||
"android": {
|
||||
"actions": [
|
||||
{
|
||||
"method": "swipe",
|
||||
"params": "up"
|
||||
},
|
||||
{
|
||||
"method": "sleep_random",
|
||||
"params": [
|
||||
5,
|
||||
10
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"loops": 1
|
||||
},
|
||||
{
|
||||
"name": "滑动 Feed 10 次,70% 随机间隔 0-5s,30% 随机间隔 5-10s",
|
||||
"android": {
|
||||
"actions": [
|
||||
{
|
||||
"method": "swipe",
|
||||
"params": "up"
|
||||
},
|
||||
{
|
||||
"method": "sleep_random",
|
||||
"params": [
|
||||
0,
|
||||
5,
|
||||
0.7,
|
||||
5,
|
||||
10,
|
||||
0.3
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"loops": 10
|
||||
},
|
||||
{
|
||||
"name": "exit",
|
||||
"android": {
|
||||
"actions": [
|
||||
{
|
||||
"method": "app_terminate",
|
||||
"params": "com.ss.android.ugc.aweme"
|
||||
}
|
||||
]
|
||||
},
|
||||
"validate": [
|
||||
{
|
||||
"check": "ui_foreground_app",
|
||||
"assert": "not_equal",
|
||||
"expect": "com.ss.android.ugc.aweme",
|
||||
"msg": "app [com.ss.android.ugc.aweme] should not be in foreground"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
62
examples/uitest/demo_android_feed_swipe_test.go
Normal file
62
examples/uitest/demo_android_feed_swipe_test.go
Normal file
@@ -0,0 +1,62 @@
|
||||
//go:build localtest
|
||||
|
||||
package uitest
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/httprunner/httprunner/v4/hrp"
|
||||
"github.com/httprunner/httprunner/v4/hrp/pkg/uixt"
|
||||
)
|
||||
|
||||
func TestAndroidDouyinFeedTest(t *testing.T) {
|
||||
testCase := &hrp.TestCase{
|
||||
Config: hrp.NewConfig("点播_抖音_滑动场景_随机间隔_android").
|
||||
WithVariables(map[string]interface{}{
|
||||
"device": "${ENV(SerialNumber)}",
|
||||
}).
|
||||
SetAndroid(uixt.WithSerialNumber("$device")),
|
||||
TestSteps: []hrp.IStep{
|
||||
hrp.NewStep("启动抖音").
|
||||
Android().
|
||||
AppTerminate("com.ss.android.ugc.aweme").
|
||||
AppLaunch("com.ss.android.ugc.aweme").
|
||||
Sleep(10).
|
||||
Validate().
|
||||
AssertAppInForeground("com.ss.android.ugc.aweme"),
|
||||
hrp.NewStep("处理青少年弹窗").
|
||||
Android().
|
||||
TapByOCR("我知道了", uixt.WithIgnoreNotFoundError(true)),
|
||||
hrp.NewStep("滑动 Feed 3 次,随机间隔 0-5s").
|
||||
Loop(3).
|
||||
Android().
|
||||
SwipeUp().
|
||||
SleepRandom(0, 5),
|
||||
hrp.NewStep("滑动 Feed 1 次,随机间隔 5-10s").
|
||||
Loop(1).
|
||||
Android().
|
||||
SwipeUp().
|
||||
SleepRandom(5, 10),
|
||||
hrp.NewStep("滑动 Feed 10 次,70% 随机间隔 0-5s,30% 随机间隔 5-10s").
|
||||
Loop(10).
|
||||
Android().
|
||||
SwipeUp().
|
||||
SleepRandom(0, 5, 0.7, 5, 10, 0.3),
|
||||
hrp.NewStep("exit").
|
||||
Android().
|
||||
AppTerminate("com.ss.android.ugc.aweme").
|
||||
Validate().
|
||||
AssertAppNotInForeground("com.ss.android.ugc.aweme"),
|
||||
},
|
||||
}
|
||||
|
||||
if err := testCase.Dump2JSON("demo_android_feed_swipe.json"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
runner := hrp.NewRunner(t).SetSaveTests(true)
|
||||
err := runner.Run(testCase)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
140
examples/uitest/demo_android_live_swipe.json
Normal file
140
examples/uitest/demo_android_live_swipe.json
Normal file
@@ -0,0 +1,140 @@
|
||||
{
|
||||
"config": {
|
||||
"name": "点播_抖音_滑动场景_随机间隔_android",
|
||||
"variables": {
|
||||
"device": "${ENV(SerialNumber)}"
|
||||
},
|
||||
"android": [
|
||||
{
|
||||
"serial": "$device"
|
||||
}
|
||||
]
|
||||
},
|
||||
"teststeps": [
|
||||
{
|
||||
"name": "启动抖音",
|
||||
"android": {
|
||||
"actions": [
|
||||
{
|
||||
"method": "app_terminate",
|
||||
"params": "com.ss.android.ugc.aweme"
|
||||
},
|
||||
{
|
||||
"method": "app_launch",
|
||||
"params": "com.ss.android.ugc.aweme"
|
||||
},
|
||||
{
|
||||
"method": "sleep",
|
||||
"params": 5
|
||||
}
|
||||
]
|
||||
},
|
||||
"validate": [
|
||||
{
|
||||
"check": "ui_foreground_app",
|
||||
"assert": "equal",
|
||||
"expect": "com.ss.android.ugc.aweme",
|
||||
"msg": "app [com.ss.android.ugc.aweme] should be in foreground"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "处理青少年弹窗",
|
||||
"android": {
|
||||
"actions": [
|
||||
{
|
||||
"method": "tap_ocr",
|
||||
"params": "我知道了",
|
||||
"ignore_NotFoundError": true
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "在推荐页上划,直到出现「点击进入直播间」",
|
||||
"android": {
|
||||
"actions": [
|
||||
{
|
||||
"method": "swipe_to_tap_text",
|
||||
"params": "点击进入直播间",
|
||||
"identifier": "进入直播间",
|
||||
"max_retry_times": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "滑动 Feed 5 次,60% 随机间隔 0-5s,40% 随机间隔 5-10s",
|
||||
"android": {
|
||||
"actions": [
|
||||
{
|
||||
"method": "swipe",
|
||||
"params": "up"
|
||||
},
|
||||
{
|
||||
"method": "sleep_random",
|
||||
"params": [
|
||||
0,
|
||||
5,
|
||||
0.6,
|
||||
5,
|
||||
10,
|
||||
0.4
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"loops": 5
|
||||
},
|
||||
{
|
||||
"name": "向上滑动,等待 10s",
|
||||
"android": {
|
||||
"actions": [
|
||||
{
|
||||
"method": "swipe",
|
||||
"params": "up",
|
||||
"identifier": "第一次上划"
|
||||
},
|
||||
{
|
||||
"method": "sleep",
|
||||
"params": 10
|
||||
},
|
||||
{
|
||||
"method": "screenshot"
|
||||
},
|
||||
{
|
||||
"method": "swipe",
|
||||
"params": "up",
|
||||
"identifier": "第二次上划"
|
||||
},
|
||||
{
|
||||
"method": "sleep",
|
||||
"params": 10
|
||||
},
|
||||
{
|
||||
"method": "screenshot"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "exit",
|
||||
"android": {
|
||||
"actions": [
|
||||
{
|
||||
"method": "app_terminate",
|
||||
"params": "com.ss.android.ugc.aweme"
|
||||
}
|
||||
]
|
||||
},
|
||||
"validate": [
|
||||
{
|
||||
"check": "ui_foreground_app",
|
||||
"assert": "not_equal",
|
||||
"expect": "com.ss.android.ugc.aweme",
|
||||
"msg": "app [com.ss.android.ugc.aweme] should not be in foreground"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
59
examples/uitest/demo_android_live_swipe_test.go
Normal file
59
examples/uitest/demo_android_live_swipe_test.go
Normal file
@@ -0,0 +1,59 @@
|
||||
//go:build localtest
|
||||
|
||||
package uitest
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/httprunner/httprunner/v4/hrp"
|
||||
"github.com/httprunner/httprunner/v4/hrp/pkg/uixt"
|
||||
)
|
||||
|
||||
func TestAndroidLiveSwipeTest(t *testing.T) {
|
||||
testCase := &hrp.TestCase{
|
||||
Config: hrp.NewConfig("点播_抖音_滑动场景_随机间隔_android").
|
||||
WithVariables(map[string]interface{}{
|
||||
"device": "${ENV(SerialNumber)}",
|
||||
}).
|
||||
SetAndroid(uixt.WithSerialNumber("$device")),
|
||||
TestSteps: []hrp.IStep{
|
||||
hrp.NewStep("启动抖音").
|
||||
Android().
|
||||
AppTerminate("com.ss.android.ugc.aweme").
|
||||
AppLaunch("com.ss.android.ugc.aweme").
|
||||
Sleep(5).
|
||||
Validate().
|
||||
AssertAppInForeground("com.ss.android.ugc.aweme"),
|
||||
hrp.NewStep("处理青少年弹窗").
|
||||
Android().
|
||||
TapByOCR("我知道了", uixt.WithIgnoreNotFoundError(true)),
|
||||
hrp.NewStep("在推荐页上划,直到出现「点击进入直播间」").
|
||||
Android().
|
||||
SwipeToTapText("点击进入直播间", uixt.WithMaxRetryTimes(10), uixt.WithIdentifier("进入直播间")),
|
||||
hrp.NewStep("滑动 Feed 5 次,60% 随机间隔 0-5s,40% 随机间隔 5-10s").
|
||||
Loop(5).
|
||||
Android().
|
||||
SwipeUp().
|
||||
SleepRandom(0, 5, 0.6, 5, 10, 0.4),
|
||||
hrp.NewStep("向上滑动,等待 10s").
|
||||
Android().
|
||||
SwipeUp(uixt.WithIdentifier("第一次上划")).Sleep(10).ScreenShot(). // 上划 1 次,等待 10s,截图保存
|
||||
SwipeUp(uixt.WithIdentifier("第二次上划")).Sleep(10).ScreenShot(), // 再上划 1 次,等待 10s,截图保存
|
||||
hrp.NewStep("exit").
|
||||
Android().
|
||||
AppTerminate("com.ss.android.ugc.aweme").
|
||||
Validate().
|
||||
AssertAppNotInForeground("com.ss.android.ugc.aweme"),
|
||||
},
|
||||
}
|
||||
|
||||
if err := testCase.Dump2JSON("demo_android_live_swipe.json"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
runner := hrp.NewRunner(t).SetSaveTests(true)
|
||||
err := runner.Run(testCase)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -45,10 +45,7 @@ func TestIOSDouyinLive(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
if err := testCase.Dump2JSON("demo_douyin_live.json"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := testCase.Dump2YAML("demo_douyin_live.yaml"); err != nil {
|
||||
if err := testCase.Dump2JSON("demo_ios_live_swipe.json"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -150,11 +150,7 @@ func NewWorldCupLive(device uixt.Device, matchName, bundleID string, duration, i
|
||||
|
||||
func (wc *WorldCupLive) getCurrentLiveTime(utcTime time.Time) error {
|
||||
utcTimeStr := utcTime.Format("15:04:05")
|
||||
fileName := filepath.Join(
|
||||
wc.resultDir, "screenshot", utcTimeStr)
|
||||
ocrTexts, err := wc.driver.GetTextsByOCR(
|
||||
uixt.WithScreenShot(fileName),
|
||||
)
|
||||
ocrTexts, err := wc.driver.GetTextsByOCR()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("get ocr texts failed")
|
||||
return err
|
||||
|
||||
15
go.mod
15
go.mod
@@ -22,13 +22,13 @@ require (
|
||||
github.com/olekukonko/tablewriter v0.0.5
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/prometheus/client_golang v1.13.0
|
||||
github.com/rs/zerolog v1.28.0
|
||||
github.com/rs/zerolog v1.29.1
|
||||
github.com/satori/go.uuid v1.2.0
|
||||
github.com/shirou/gopsutil v3.21.11+incompatible
|
||||
github.com/spf13/cobra v1.5.0
|
||||
github.com/stretchr/testify v1.8.0
|
||||
gocv.io/x/gocv v0.31.0
|
||||
golang.org/x/net v0.0.0-20220919232410-f2f64ebce3c1
|
||||
golang.org/x/net v0.7.0
|
||||
golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1
|
||||
google.golang.org/grpc v1.49.0
|
||||
google.golang.org/protobuf v1.28.1
|
||||
@@ -56,7 +56,7 @@ require (
|
||||
github.com/josharian/intern v1.0.0 // 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-isatty v0.0.18 // 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
|
||||
@@ -73,12 +73,11 @@ require (
|
||||
github.com/tklauser/go-sysconf v0.3.10 // indirect
|
||||
github.com/tklauser/numcpus v0.5.0 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.2 // indirect
|
||||
golang.org/x/mod v0.4.2 // indirect
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
|
||||
golang.org/x/sync v0.0.0-20220907140024-f12130a52804 // indirect
|
||||
golang.org/x/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
|
||||
golang.org/x/sys v0.7.0 // indirect
|
||||
golang.org/x/text v0.7.0 // indirect
|
||||
golang.org/x/tools v0.1.12 // 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
|
||||
|
||||
28
go.sum
28
go.sum
@@ -89,7 +89,7 @@ github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWH
|
||||
github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
@@ -301,8 +301,9 @@ github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd
|
||||
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
|
||||
github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
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=
|
||||
@@ -370,8 +371,8 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR
|
||||
github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.26.1/go.mod h1:/wSSJWX7lVrsOwlbyTRSOJvqRlc+WjWlfes+CiJ+tmc=
|
||||
github.com/rs/zerolog v1.28.0 h1:MirSo27VyNi7RJYP3078AA1+Cyzd2GB66qy3aUHvsWY=
|
||||
github.com/rs/zerolog v1.28.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0=
|
||||
github.com/rs/zerolog v1.29.1 h1:cO+d60CHkknCbvzEWxP0S9K6KqyTjrCNUy1LdQLCGPc=
|
||||
github.com/rs/zerolog v1.29.1/go.mod h1:Le6ESbR7hc+DP6Lt1THiV8CQSdkkNrd3R0XbEgp3ZBU=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
|
||||
@@ -463,8 +464,9 @@ 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/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/net v0.0.0-20180530234432-1e491301e022/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/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=
|
||||
@@ -512,8 +514,8 @@ golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su
|
||||
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/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
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=
|
||||
@@ -625,8 +627,9 @@ golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
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/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
|
||||
golang.org/x/sys v0.7.0/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=
|
||||
@@ -637,8 +640,9 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/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=
|
||||
@@ -694,15 +698,15 @@ 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/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
|
||||
golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f 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=
|
||||
|
||||
@@ -361,6 +361,10 @@ func (b *HRPBoomer) convertBoomerTask(testcase *TestCase, rendezvousList []*Rend
|
||||
}
|
||||
mutex.Unlock()
|
||||
|
||||
defer func() {
|
||||
sessionRunner.releaseResources()
|
||||
}()
|
||||
|
||||
startTime := time.Now()
|
||||
for _, step := range testcase.TestSteps {
|
||||
// TODO: parse step struct
|
||||
|
||||
@@ -10,7 +10,7 @@ func TestBoomerStandaloneRun(t *testing.T) {
|
||||
defer removeHashicorpGoPlugin()
|
||||
|
||||
testcase1 := &TestCase{
|
||||
Config: NewConfig("TestCase1").SetBaseURL("http://httpbin.org"),
|
||||
Config: NewConfig("TestCase1").SetBaseURL("https://httpbin.org"),
|
||||
TestSteps: []IStep{
|
||||
NewStep("headers").
|
||||
GET("/headers").
|
||||
|
||||
@@ -5,10 +5,8 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/httprunner/httprunner/v4/hrp/pkg/gadb"
|
||||
"github.com/httprunner/httprunner/v4/hrp/pkg/uixt"
|
||||
)
|
||||
|
||||
@@ -21,22 +19,10 @@ var listAndroidDevicesCmd = &cobra.Command{
|
||||
Use: "devices",
|
||||
Short: "List all Android devices",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
devices, err := uixt.DeviceList()
|
||||
deviceList, err := uixt.GetAndroidDevices(serial)
|
||||
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)
|
||||
fmt.Println(err)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
for _, d := range deviceList {
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
package adb
|
||||
|
||||
import "github.com/spf13/cobra"
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/httprunner/httprunner/v4/hrp/pkg/gadb"
|
||||
"github.com/httprunner/httprunner/v4/hrp/pkg/uixt"
|
||||
)
|
||||
|
||||
var androidRootCmd = &cobra.Command{
|
||||
Use: "adb",
|
||||
@@ -8,6 +15,17 @@ var androidRootCmd = &cobra.Command{
|
||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {},
|
||||
}
|
||||
|
||||
func getDevice(serial string) (*gadb.Device, error) {
|
||||
devices, err := uixt.GetAndroidDevices(serial)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(devices) > 1 {
|
||||
return nil, fmt.Errorf("found multiple attached devices, please specify android serial")
|
||||
}
|
||||
return devices[0], nil
|
||||
}
|
||||
|
||||
func Init(rootCmd *cobra.Command) {
|
||||
rootCmd.AddCommand(androidRootCmd)
|
||||
}
|
||||
|
||||
38
hrp/cmd/adb/screencap.go
Normal file
38
hrp/cmd/adb/screencap.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package adb
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
|
||||
)
|
||||
|
||||
var screencapAndroidDevicesCmd = &cobra.Command{
|
||||
Use: "screencap",
|
||||
Short: "Start android screen capture",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
device, err := getDevice(serial)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res, err := device.ScreenCap()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
filepath := fmt.Sprintf("%s.png", builtin.GenNameWithTimestamp("screencap_%d"))
|
||||
if err = ioutil.WriteFile(filepath, res, 0o644); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println("screencap saved to", filepath)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
screencapAndroidDevicesCmd.Flags().StringVarP(&serial, "serial", "s", "", "filter by device's serial")
|
||||
androidRootCmd.AddCommand(screencapAndroidDevicesCmd)
|
||||
}
|
||||
@@ -70,18 +70,10 @@ var listDevicesCmd = &cobra.Command{
|
||||
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)
|
||||
devices, err := uixt.GetIOSDevices(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)
|
||||
}
|
||||
fmt.Println(err)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
for _, d := range devices {
|
||||
|
||||
@@ -2,7 +2,6 @@ package ios
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
@@ -16,16 +15,12 @@ var iosRootCmd = &cobra.Command{
|
||||
}
|
||||
|
||||
func getDevice(udid string) (gidevice.Device, error) {
|
||||
devices, err := uixt.IOSDevices(udid)
|
||||
devices, err := uixt.GetIOSDevices(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 nil, fmt.Errorf("found multiple attached devices, please specify ios udid")
|
||||
}
|
||||
return devices[0], nil
|
||||
}
|
||||
|
||||
@@ -24,6 +24,8 @@ var Functions = map[string]interface{}{
|
||||
"get_timestamp": getTimestamp, // call without arguments
|
||||
"sleep": sleep, // call with one argument
|
||||
"gen_random_string": genRandomString, // call with one argument
|
||||
"random_int": rand.Intn, // call with one argument
|
||||
"random_range": random_range, // call with two arguments
|
||||
"max": math.Max, // call with two arguments
|
||||
"md5": MD5, // call with one argument
|
||||
"parameterize": loadFromCSV,
|
||||
@@ -49,6 +51,10 @@ func init() {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
}
|
||||
|
||||
func random_range(a, b float64) float64 {
|
||||
return a + rand.Float64()*(b-a)
|
||||
}
|
||||
|
||||
func getTimestamp() int64 {
|
||||
return time.Now().UnixNano() / int64(time.Millisecond)
|
||||
}
|
||||
|
||||
@@ -443,3 +443,10 @@ func Sign(ver string, ak string, sk string, body []byte) string {
|
||||
signResult := sha256HMAC(signKey, body)
|
||||
return fmt.Sprintf("%v/%v", signKeyInfo, string(signResult))
|
||||
}
|
||||
|
||||
func GenNameWithTimestamp(tmpl string) string {
|
||||
if !strings.Contains(tmpl, "%d") {
|
||||
tmpl = tmpl + "_%d"
|
||||
}
|
||||
return fmt.Sprintf(tmpl, time.Now().Unix())
|
||||
}
|
||||
|
||||
@@ -67,8 +67,9 @@ var (
|
||||
|
||||
// UI automation related: [70, 80)
|
||||
var (
|
||||
MobileUIDriverError = errors.New("mobile UI driver error") // 70
|
||||
MobileUIValidationError = errors.New("mobile UI validation error") // 75
|
||||
MobileUIDriverError = errors.New("mobile UI driver error") // 70
|
||||
MobileUIValidationError = errors.New("mobile UI validation error") // 75
|
||||
MobileUIAppNotInForegroundError = errors.New("mobile UI app not in foreground error") // 76
|
||||
)
|
||||
|
||||
// OCR related: [80, 90)
|
||||
@@ -123,8 +124,9 @@ var errorsMap = map[error]int{
|
||||
AndroidCaptureLogError: 66,
|
||||
|
||||
// UI automation related
|
||||
MobileUIDriverError: 70,
|
||||
MobileUIValidationError: 75,
|
||||
MobileUIDriverError: 70,
|
||||
MobileUIValidationError: 75,
|
||||
MobileUIAppNotInForegroundError: 76,
|
||||
|
||||
// OCR related
|
||||
OCREnvMissedError: 80,
|
||||
|
||||
@@ -1 +1 @@
|
||||
v4.3.2
|
||||
v4.3.3
|
||||
@@ -38,8 +38,8 @@ type iteratorStrategy struct {
|
||||
PickOrder iteratorPickOrder `json:"pick_order,omitempty" yaml:"pick_order,omitempty"`
|
||||
}
|
||||
|
||||
func initParametersIterator(cfg *TConfig) (*ParametersIterator, error) {
|
||||
parameters, err := loadParameters(cfg.Parameters, cfg.Variables)
|
||||
func (p *Parser) initParametersIterator(cfg *TConfig) (*ParametersIterator, error) {
|
||||
parameters, err := p.loadParameters(cfg.Parameters, cfg.Variables)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -236,7 +236,7 @@ configParameters = {
|
||||
]
|
||||
}
|
||||
*/
|
||||
func loadParameters(configParameters map[string]interface{}, variablesMapping map[string]interface{}) (
|
||||
func (p *Parser) loadParameters(configParameters map[string]interface{}, variablesMapping map[string]interface{}) (
|
||||
map[string]Parameters, error) {
|
||||
|
||||
if len(configParameters) == 0 {
|
||||
@@ -263,7 +263,7 @@ func loadParameters(configParameters map[string]interface{}, variablesMapping ma
|
||||
// => [["test1", "111111"], ["test2", "222222"]]
|
||||
// e.g. "app_version": "${gen_app_version()}"
|
||||
// => ["1.0.0", "1.0.1"]
|
||||
parsedParameterContent, err := newParser().ParseString(rawValue.String(), variablesMapping)
|
||||
parsedParameterContent, err := p.ParseString(rawValue.String(), variablesMapping)
|
||||
if err != nil {
|
||||
log.Error().Err(err).
|
||||
Str("parametersRawContent", rawValue.String()).
|
||||
|
||||
@@ -74,8 +74,9 @@ func TestLoadParameters(t *testing.T) {
|
||||
variablesMapping := map[string]interface{}{
|
||||
"file": "account.csv",
|
||||
}
|
||||
parser := newParser()
|
||||
for _, data := range testData {
|
||||
value, err := loadParameters(data.configParameters, variablesMapping)
|
||||
value, err := parser.loadParameters(data.configParameters, variablesMapping)
|
||||
if !assert.Nil(t, err) {
|
||||
t.Fatal()
|
||||
}
|
||||
@@ -92,21 +93,25 @@ func TestLoadParametersError(t *testing.T) {
|
||||
{
|
||||
map[string]interface{}{
|
||||
"username_password": fmt.Sprintf("${parameterize(%s/account.csv)}", hrpExamplesDir),
|
||||
"user_agent": []interface{}{"iOS/10.1", "iOS/10.2"}},
|
||||
"user_agent": []interface{}{"iOS/10.1", "iOS/10.2"},
|
||||
},
|
||||
},
|
||||
{
|
||||
map[string]interface{}{
|
||||
"username-password": fmt.Sprintf("${parameterize(%s/account.csv)}", hrpExamplesDir),
|
||||
"user-agent": []interface{}{"iOS/10.1", "iOS/10.2"}},
|
||||
"user-agent": []interface{}{"iOS/10.1", "iOS/10.2"},
|
||||
},
|
||||
},
|
||||
{
|
||||
map[string]interface{}{
|
||||
"username-password": fmt.Sprintf("${param(%s/account.csv)}", hrpExamplesDir),
|
||||
"user_agent": []interface{}{"iOS/10.1", "iOS/10.2"}},
|
||||
"user_agent": []interface{}{"iOS/10.1", "iOS/10.2"},
|
||||
},
|
||||
},
|
||||
}
|
||||
parser := newParser()
|
||||
for _, data := range testData {
|
||||
_, err := loadParameters(data.configParameters, map[string]interface{}{})
|
||||
_, err := parser.loadParameters(data.configParameters, map[string]interface{}{})
|
||||
if !assert.Error(t, err) {
|
||||
t.Fatal()
|
||||
}
|
||||
@@ -240,8 +245,9 @@ func TestInitParametersIteratorCount(t *testing.T) {
|
||||
1,
|
||||
},
|
||||
}
|
||||
parser := newParser()
|
||||
for _, data := range testData {
|
||||
iterator, err := initParametersIterator(data.cfg)
|
||||
iterator, err := parser.initParametersIterator(data.cfg)
|
||||
if !assert.Nil(t, err) {
|
||||
t.Fatal()
|
||||
}
|
||||
@@ -288,8 +294,9 @@ func TestInitParametersIteratorUnlimitedCount(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
parser := newParser()
|
||||
for _, data := range testData {
|
||||
iterator, err := initParametersIterator(data.cfg)
|
||||
iterator, err := parser.initParametersIterator(data.cfg)
|
||||
if !assert.Nil(t, err) {
|
||||
t.Fatal()
|
||||
}
|
||||
@@ -370,8 +377,9 @@ func TestInitParametersIteratorContent(t *testing.T) {
|
||||
map[string]interface{}{},
|
||||
},
|
||||
}
|
||||
parser := newParser()
|
||||
for _, data := range testData {
|
||||
iterator, err := initParametersIterator(data.cfg)
|
||||
iterator, err := parser.initParametersIterator(data.cfg)
|
||||
if !assert.Nil(t, err) {
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# gadb
|
||||
|
||||
This module is initially forked from [electricbubble/gadb@v0.0.7].
|
||||
This module is initially forked from [electricbubble/gadb@v0.0.7] and optimized by [@appl3s].
|
||||
|
||||
- feat: add reverse forward command
|
||||
- feat: add `RunShellCommandV2` which supports running nohup
|
||||
- feat: add `InstallAPK` with feature judgment
|
||||
- feat: add `Uninstall` for specified package name
|
||||
|
||||
[electricbubble/gadb@v0.0.7]: https://github.com/electricbubble/gadb/tree/v0.0.7
|
||||
[@appl3s]: https://github.com/appl3s
|
||||
|
||||
@@ -38,6 +38,16 @@ func NewClientWith(host string, port ...int) (adbClient Client, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func NewClientWithoutTransport(host string, port ...int) (adbClient Client, err error) {
|
||||
if len(port) == 0 {
|
||||
port = []int{AdbServerPort}
|
||||
}
|
||||
adbClient.host = host
|
||||
adbClient.port = port[0]
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (c Client) ServerVersion() (version int, err error) {
|
||||
var resp string
|
||||
if resp, err = c.executeCommand("host:version"); err != nil {
|
||||
@@ -73,14 +83,14 @@ func (c Client) DeviceSerialList() (serials []string, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (c Client) DeviceList() (devices []Device, err error) {
|
||||
func (c Client) DeviceList() (devices []*Device, err error) {
|
||||
var resp string
|
||||
if resp, err = c.executeCommand("host:devices-l"); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
lines := strings.Split(resp, "\n")
|
||||
devices = make([]Device, 0, len(lines))
|
||||
devices = make([]*Device, 0, len(lines))
|
||||
|
||||
for i := range lines {
|
||||
line := strings.TrimSpace(lines[i])
|
||||
@@ -101,7 +111,7 @@ func (c Client) DeviceList() (devices []Device, err error) {
|
||||
key, val := split[0], split[1]
|
||||
mapAttrs[key] = val
|
||||
}
|
||||
devices = append(devices, Device{adbClient: c, serial: fields[0], attrs: mapAttrs})
|
||||
devices = append(devices, &Device{adbClient: c, serial: fields[0], attrs: mapAttrs})
|
||||
}
|
||||
|
||||
return
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
package gadb
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -127,3 +128,22 @@ func TestClient_KillServer(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestScreenCap(t *testing.T) {
|
||||
adbClient, err := NewClient()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
dl, err := adbClient.DeviceList()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
d := dl[0]
|
||||
res, err := d.ScreenCap()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
t.Log(len(res))
|
||||
ioutil.WriteFile("/tmp/1.png", res, 0o644)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package gadb
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -9,6 +11,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
|
||||
)
|
||||
|
||||
type DeviceFileInfo struct {
|
||||
@@ -48,9 +52,10 @@ func deviceStateConv(k string) (deviceState DeviceState) {
|
||||
}
|
||||
|
||||
type DeviceForward struct {
|
||||
Serial string
|
||||
Local string
|
||||
Remote string
|
||||
Serial string
|
||||
Local string
|
||||
Remote string
|
||||
Reverse bool
|
||||
// LocalProtocol string
|
||||
// RemoteProtocol string
|
||||
}
|
||||
@@ -59,51 +64,92 @@ type Device struct {
|
||||
adbClient Client
|
||||
serial string
|
||||
attrs map[string]string
|
||||
feat Features
|
||||
}
|
||||
|
||||
func (d Device) Product() string {
|
||||
func (d *Device) HasFeature(name Feature) bool {
|
||||
feats, err := d.GetFeatures()
|
||||
if err != nil || len(feats) == 0 {
|
||||
return false
|
||||
}
|
||||
return feats.HasFeature(name)
|
||||
}
|
||||
|
||||
func (d *Device) GetFeatures() (features Features, err error) {
|
||||
if len(d.feat) > 0 {
|
||||
return d.feat, nil
|
||||
}
|
||||
return d.features()
|
||||
}
|
||||
|
||||
func (d *Device) features() (features Features, err error) {
|
||||
res, err := d.executeCommand("host:features")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(res) > 4 {
|
||||
// stip hash
|
||||
res = res[4:]
|
||||
}
|
||||
fs := strings.Split(string(res), ",")
|
||||
features = make(Features, len(fs))
|
||||
for _, f := range fs {
|
||||
features[Feature(f)] = struct{}{}
|
||||
}
|
||||
d.feat = features
|
||||
return features, nil
|
||||
}
|
||||
|
||||
func (d *Device) Product() string {
|
||||
return d.attrs["product"]
|
||||
}
|
||||
|
||||
func (d Device) Model() string {
|
||||
func (d *Device) Model() string {
|
||||
return d.attrs["model"]
|
||||
}
|
||||
|
||||
func (d Device) Usb() string {
|
||||
func (d *Device) Usb() string {
|
||||
return d.attrs["usb"]
|
||||
}
|
||||
|
||||
func (d Device) transportId() string {
|
||||
func (d *Device) transportId() string {
|
||||
return d.attrs["transport_id"]
|
||||
}
|
||||
|
||||
func (d Device) DeviceInfo() map[string]string {
|
||||
func (d *Device) DeviceInfo() map[string]string {
|
||||
return d.attrs
|
||||
}
|
||||
|
||||
func (d Device) Serial() string {
|
||||
func (d *Device) Serial() string {
|
||||
// resp, err := d.adbClient.executeCommand(fmt.Sprintf("host-serial:%s:get-serialno", d.serial))
|
||||
return d.serial
|
||||
}
|
||||
|
||||
func (d Device) IsUsb() bool {
|
||||
func (d *Device) IsUsb() bool {
|
||||
return d.Usb() != ""
|
||||
}
|
||||
|
||||
func (d Device) State() (DeviceState, error) {
|
||||
func (d *Device) State() (DeviceState, error) {
|
||||
resp, err := d.adbClient.executeCommand(fmt.Sprintf("host-serial:%s:get-state", d.serial))
|
||||
return deviceStateConv(resp), err
|
||||
}
|
||||
|
||||
func (d Device) DevicePath() (string, error) {
|
||||
func (d *Device) DevicePath() (string, error) {
|
||||
resp, err := d.adbClient.executeCommand(fmt.Sprintf("host-serial:%s:get-devpath", d.serial))
|
||||
return resp, err
|
||||
}
|
||||
|
||||
func (d Device) Forward(localPort, remotePort int, noRebind ...bool) (err error) {
|
||||
func (d *Device) Forward(localPort int, remoteInterface interface{}, noRebind ...bool) (err error) {
|
||||
command := ""
|
||||
var remote string
|
||||
local := fmt.Sprintf("tcp:%d", localPort)
|
||||
remote := fmt.Sprintf("tcp:%d", remotePort)
|
||||
switch r := remoteInterface.(type) {
|
||||
// for unix sockets
|
||||
case string:
|
||||
remote = r
|
||||
case int:
|
||||
remote = fmt.Sprintf("tcp:%d", r)
|
||||
}
|
||||
|
||||
if len(noRebind) != 0 && noRebind[0] {
|
||||
command = fmt.Sprintf("host-serial:%s:forward:norebind:%s;%s", d.serial, local, remote)
|
||||
@@ -115,7 +161,7 @@ func (d Device) Forward(localPort, remotePort int, noRebind ...bool) (err error)
|
||||
return
|
||||
}
|
||||
|
||||
func (d Device) ForwardList() (deviceForwardList []DeviceForward, err error) {
|
||||
func (d *Device) ForwardList() (deviceForwardList []DeviceForward, err error) {
|
||||
var forwardList []DeviceForward
|
||||
if forwardList, err = d.adbClient.ForwardList(); err != nil {
|
||||
return nil, err
|
||||
@@ -131,18 +177,80 @@ func (d Device) ForwardList() (deviceForwardList []DeviceForward, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (d Device) ForwardKill(localPort int) (err error) {
|
||||
func (d *Device) ForwardKill(localPort int) (err error) {
|
||||
local := fmt.Sprintf("tcp:%d", localPort)
|
||||
_, err = d.adbClient.executeCommand(fmt.Sprintf("host-serial:%s:killforward:%s", d.serial, local), true)
|
||||
return
|
||||
}
|
||||
|
||||
func (d Device) RunShellCommand(cmd string, args ...string) (string, error) {
|
||||
func (d *Device) ReverseForward(localPort int, remoteInterface interface{}, noRebind ...bool) (err error) {
|
||||
var command string
|
||||
var remote string
|
||||
local := fmt.Sprintf("tcp:%d", localPort)
|
||||
switch r := remoteInterface.(type) {
|
||||
// for unix sockets
|
||||
case string:
|
||||
remote = r
|
||||
case int:
|
||||
remote = fmt.Sprintf("tcp:%d", r)
|
||||
}
|
||||
|
||||
if len(noRebind) != 0 && noRebind[0] {
|
||||
command = fmt.Sprintf("reverse:forward:norebind:%s;%s", remote, local)
|
||||
} else {
|
||||
command = fmt.Sprintf("reverse:forward:%s;%s", remote, local)
|
||||
}
|
||||
_, err = d.executeCommand(command, true)
|
||||
return
|
||||
}
|
||||
|
||||
func (d *Device) ReverseForwardList() (deviceForwardList []DeviceForward, err error) {
|
||||
res, err := d.executeCommand("reverse:list-forward")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resStr := string(res)
|
||||
lines := strings.Split(resStr, "\n")
|
||||
for _, line := range lines {
|
||||
groups := strings.Split(line, " ")
|
||||
if len(groups) == 3 {
|
||||
deviceForwardList = append(deviceForwardList, DeviceForward{
|
||||
Reverse: true,
|
||||
Serial: d.serial,
|
||||
Remote: groups[1],
|
||||
Local: groups[2],
|
||||
})
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (d *Device) ReverseForwardKill(remoteInterface interface{}) error {
|
||||
remote := ""
|
||||
switch r := remoteInterface.(type) {
|
||||
case string:
|
||||
remote = r
|
||||
case int:
|
||||
remote = fmt.Sprintf("tcp:%d", r)
|
||||
}
|
||||
_, err := d.executeCommand(fmt.Sprintf("reverse:killforward:%s", remote), true)
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *Device) ReverseForwardKillAll() error {
|
||||
_, err := d.executeCommand("reverse:killforward-all")
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *Device) RunShellCommand(cmd string, args ...string) (string, error) {
|
||||
raw, err := d.RunShellCommandWithBytes(cmd, args...)
|
||||
return string(raw), err
|
||||
}
|
||||
|
||||
func (d Device) RunShellCommandWithBytes(cmd string, args ...string) ([]byte, error) {
|
||||
func (d *Device) RunShellCommandWithBytes(cmd string, args ...string) ([]byte, error) {
|
||||
if d.HasFeature(FeatShellV2) {
|
||||
return d.RunShellCommandV2WithBytes(cmd, args...)
|
||||
}
|
||||
if len(args) > 0 {
|
||||
cmd = fmt.Sprintf("%s %s", cmd, strings.Join(args, " "))
|
||||
}
|
||||
@@ -156,7 +264,86 @@ func (d Device) RunShellCommandWithBytes(cmd string, args ...string) ([]byte, er
|
||||
return raw, err
|
||||
}
|
||||
|
||||
func (d Device) EnableAdbOverTCP(port ...int) (err error) {
|
||||
func (d *Device) RunShellCommandV2(cmd string, args ...string) (string, error) {
|
||||
raw, err := d.RunShellCommandV2WithBytes(cmd, args...)
|
||||
return string(raw), err
|
||||
}
|
||||
|
||||
// RunShellCommandV2WithBytes shell v2, 支持后台运行而不会阻断
|
||||
func (d *Device) RunShellCommandV2WithBytes(cmd string, args ...string) ([]byte, error) {
|
||||
if len(args) > 0 {
|
||||
cmd = fmt.Sprintf("%s %s", cmd, strings.Join(args, " "))
|
||||
}
|
||||
if strings.TrimSpace(cmd) == "" {
|
||||
return nil, errors.New("adb shell: command cannot be empty")
|
||||
}
|
||||
log.Debug().Str("cmd",
|
||||
fmt.Sprintf("adb -s %s shell %s", d.serial, cmd)).
|
||||
Msg("run adb command in v2")
|
||||
raw, err := d.executeCommand(fmt.Sprintf("shell,v2,raw:%s", cmd))
|
||||
if err != nil {
|
||||
return raw, err
|
||||
}
|
||||
return d.parseV2CommandWithBytes(raw)
|
||||
}
|
||||
|
||||
func (d *Device) parseV2CommandWithBytes(input []byte) (res []byte, err error) {
|
||||
if len(input) == 0 {
|
||||
return input, nil
|
||||
}
|
||||
reader := bytes.NewReader(input)
|
||||
sizeBuf := make([]byte, 4)
|
||||
var (
|
||||
resBuf []byte
|
||||
exitCode int
|
||||
)
|
||||
loop:
|
||||
for {
|
||||
msgCode, err := reader.ReadByte()
|
||||
if err != nil {
|
||||
return input, err
|
||||
}
|
||||
switch msgCode {
|
||||
case 0x01, 0x02: // STDOUT, STDERR
|
||||
_, err = io.ReadFull(reader, sizeBuf)
|
||||
if err != nil {
|
||||
return input, err
|
||||
}
|
||||
size := binary.LittleEndian.Uint32(sizeBuf)
|
||||
if cap(resBuf) < int(size) {
|
||||
resBuf = make([]byte, int(size))
|
||||
}
|
||||
_, err = io.ReadFull(reader, resBuf[:size])
|
||||
if err != nil {
|
||||
return input, err
|
||||
}
|
||||
res = append(res, resBuf[:size]...)
|
||||
case 0x03: // EXIT
|
||||
_, err = io.ReadFull(reader, sizeBuf)
|
||||
if err != nil {
|
||||
return input, err
|
||||
}
|
||||
size := binary.LittleEndian.Uint32(sizeBuf)
|
||||
if cap(resBuf) < int(size) {
|
||||
resBuf = make([]byte, int(size))
|
||||
}
|
||||
ec, err := reader.ReadByte()
|
||||
if err != nil {
|
||||
return input, err
|
||||
}
|
||||
exitCode = int(ec)
|
||||
break loop
|
||||
default:
|
||||
return input, nil
|
||||
}
|
||||
}
|
||||
if exitCode != 0 {
|
||||
return nil, errors.New(string(res))
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (d *Device) EnableAdbOverTCP(port ...int) (err error) {
|
||||
if len(port) == 0 {
|
||||
port = []int{AdbDaemonPort}
|
||||
}
|
||||
@@ -168,7 +355,7 @@ func (d Device) EnableAdbOverTCP(port ...int) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (d Device) createDeviceTransport() (tp transport, err error) {
|
||||
func (d *Device) createDeviceTransport() (tp transport, err error) {
|
||||
if tp, err = newTransport(fmt.Sprintf("%s:%d", d.adbClient.host, d.adbClient.port)); err != nil {
|
||||
return transport{}, err
|
||||
}
|
||||
@@ -180,7 +367,7 @@ func (d Device) createDeviceTransport() (tp transport, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (d Device) executeCommand(command string, onlyVerifyResponse ...bool) (raw []byte, err error) {
|
||||
func (d *Device) executeCommand(command string, onlyVerifyResponse ...bool) (raw []byte, err error) {
|
||||
if len(onlyVerifyResponse) == 0 {
|
||||
onlyVerifyResponse = []bool{false}
|
||||
}
|
||||
@@ -207,7 +394,7 @@ func (d Device) executeCommand(command string, onlyVerifyResponse ...bool) (raw
|
||||
return
|
||||
}
|
||||
|
||||
func (d Device) List(remotePath string) (devFileInfos []DeviceFileInfo, err error) {
|
||||
func (d *Device) List(remotePath string) (devFileInfos []DeviceFileInfo, err error) {
|
||||
var tp transport
|
||||
if tp, err = d.createDeviceTransport(); err != nil {
|
||||
return nil, err
|
||||
@@ -237,7 +424,7 @@ func (d Device) List(remotePath string) (devFileInfos []DeviceFileInfo, err erro
|
||||
return
|
||||
}
|
||||
|
||||
func (d Device) PushFile(local *os.File, remotePath string, modification ...time.Time) (err error) {
|
||||
func (d *Device) PushFile(local *os.File, remotePath string, modification ...time.Time) (err error) {
|
||||
if len(modification) == 0 {
|
||||
var stat os.FileInfo
|
||||
if stat, err = local.Stat(); err != nil {
|
||||
@@ -249,7 +436,7 @@ func (d Device) PushFile(local *os.File, remotePath string, modification ...time
|
||||
return d.Push(local, remotePath, modification[0], DefaultFileMode)
|
||||
}
|
||||
|
||||
func (d Device) Push(source io.Reader, remotePath string, modification time.Time, mode ...os.FileMode) (err error) {
|
||||
func (d *Device) Push(source io.Reader, remotePath string, modification time.Time, mode ...os.FileMode) (err error) {
|
||||
if len(mode) == 0 {
|
||||
mode = []os.FileMode{DefaultFileMode}
|
||||
}
|
||||
@@ -285,7 +472,7 @@ func (d Device) Push(source io.Reader, remotePath string, modification time.Time
|
||||
return
|
||||
}
|
||||
|
||||
func (d Device) Pull(remotePath string, dest io.Writer) (err error) {
|
||||
func (d *Device) Pull(remotePath string, dest io.Writer) (err error) {
|
||||
var tp transport
|
||||
if tp, err = d.createDeviceTransport(); err != nil {
|
||||
return err
|
||||
@@ -305,3 +492,102 @@ func (d Device) Pull(remotePath string, dest io.Writer) (err error) {
|
||||
err = sync.WriteStream(dest)
|
||||
return
|
||||
}
|
||||
|
||||
func (d *Device) installViaABBExec(apk io.ReadSeeker) (raw []byte, err error) {
|
||||
var (
|
||||
tp transport
|
||||
filesize int64
|
||||
)
|
||||
filesize, err = apk.Seek(0, io.SeekEnd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tp, err = d.createDeviceTransport(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() { _ = tp.Close() }()
|
||||
if err = tp.Send(fmt.Sprintf("abb_exec:package\x00install\x00-t\x00-S\x00%d", filesize)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = tp.VerifyResponse(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = apk.Seek(0, io.SeekStart)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = io.Copy(tp.Conn(), apk)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
raw, err = tp.ReadBytesAll()
|
||||
return
|
||||
}
|
||||
|
||||
func (d *Device) InstallAPK(apk io.ReadSeeker) (string, error) {
|
||||
haserr := func(ret string) bool {
|
||||
return strings.Contains(ret, "Failure")
|
||||
}
|
||||
if d.HasFeature(FeatAbbExec) {
|
||||
raw, err := d.installViaABBExec(apk)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error installing: %v", err)
|
||||
}
|
||||
if haserr(string(raw)) {
|
||||
return "", errors.New(string(raw))
|
||||
}
|
||||
return string(raw), err
|
||||
}
|
||||
|
||||
remote := fmt.Sprintf("/data/local/tmp/%s.apk", builtin.GenNameWithTimestamp("gadb_remote_%d"))
|
||||
err := d.Push(apk, remote, time.Now())
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error pushing: %v", err)
|
||||
}
|
||||
|
||||
res, err := d.RunShellCommand("pm", "install", "-f", remote)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error installing: %v", err)
|
||||
}
|
||||
if haserr(res) {
|
||||
return "", errors.New(res)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (d *Device) Uninstall(packageName string, keepData ...bool) (string, error) {
|
||||
if len(keepData) == 0 {
|
||||
keepData = []bool{false}
|
||||
}
|
||||
packageName = strings.ReplaceAll(packageName, " ", "")
|
||||
if len(packageName) == 0 {
|
||||
return "", fmt.Errorf("invalid package name")
|
||||
}
|
||||
args := []string{"uninstall"}
|
||||
if keepData[0] {
|
||||
args = append(args, "-k")
|
||||
}
|
||||
args = append(args, packageName)
|
||||
return d.RunShellCommandV2("pm", args...)
|
||||
}
|
||||
|
||||
func (d *Device) ScreenCap() ([]byte, error) {
|
||||
if d.HasFeature(FeatShellV2) {
|
||||
return d.RunShellCommandV2WithBytes("screencap", "-p")
|
||||
}
|
||||
|
||||
// for shell v1, screenshot buffer maybe truncated
|
||||
// thus we firstly save it to local file and then pull it
|
||||
tempPath := fmt.Sprintf("/data/local/tmp/screenshot_%d.png",
|
||||
time.Now().Unix())
|
||||
_, err := d.RunShellCommandWithBytes("screencap", "-p", tempPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
err = d.Pull(tempPath, buffer)
|
||||
return buffer.Bytes(), err
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -145,6 +146,42 @@ func TestDevice_Forward(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDevice_ReverseForward(t *testing.T) {
|
||||
adbClient, err := NewClient()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
devices, err := adbClient.DeviceList()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
localPort := 5005
|
||||
err = devices[0].ReverseForward(localPort, "localabstract:scrcpy")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = devices[0].ReverseForward(localPort, "localabstract:scrcpy1")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = devices[0].ReverseForwardList()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = devices[0].ReverseForwardKill("localabstract:scrcpy1")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = devices[0].ReverseForwardKillAll()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDevice_ForwardList(t *testing.T) {
|
||||
adbClient, err := NewClient()
|
||||
if err != nil {
|
||||
@@ -314,3 +351,94 @@ func TestDevice_Pull(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDevice_RunShellCommandBackgroundWithBytes(t *testing.T) {
|
||||
type fields struct {
|
||||
adbClient Client
|
||||
serial string
|
||||
attrs map[string]string
|
||||
}
|
||||
type args struct {
|
||||
cmd string
|
||||
args []string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want []byte
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "runShellCommandBackground",
|
||||
fields: fields{
|
||||
adbClient: func() Client {
|
||||
c, _ := NewClient()
|
||||
return c
|
||||
}(),
|
||||
serial: "63c1ee94",
|
||||
},
|
||||
args: args{
|
||||
cmd: "nohup sleep 10 2>/dev/null 1>/dev/null &",
|
||||
// cmd: "sleep 10",
|
||||
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
d := Device{
|
||||
adbClient: tt.fields.adbClient,
|
||||
serial: tt.fields.serial,
|
||||
attrs: tt.fields.attrs,
|
||||
}
|
||||
got, err := d.RunShellCommandV2WithBytes(tt.args.cmd, tt.args.args...)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("Device.RunShellCommandBackgroundWithBytes() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("Device.RunShellCommandBackgroundWithBytes() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDevice_InstallAPK(t *testing.T) {
|
||||
apk, _ := os.Open("test.apk")
|
||||
adbClient, err := NewClient()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
devices, err := adbClient.DeviceList()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
dev := devices[len(devices)-1]
|
||||
dev = devices[0]
|
||||
|
||||
res, err := dev.InstallAPK(apk)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(res)
|
||||
}
|
||||
|
||||
func TestDevice_HasFeature(t *testing.T) {
|
||||
adbClient, err := NewClient()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
devices, err := adbClient.DeviceList()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
dev := devices[len(devices)-1]
|
||||
dev = devices[0]
|
||||
|
||||
t.Log(dev.GetFeatures())
|
||||
}
|
||||
|
||||
26
hrp/pkg/gadb/features.go
Normal file
26
hrp/pkg/gadb/features.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package gadb
|
||||
|
||||
type (
|
||||
Feature string
|
||||
Features map[Feature]struct{}
|
||||
)
|
||||
|
||||
var (
|
||||
FeatSendrecvV2Brotli = Feature("sendrecv_v2_brotli")
|
||||
FeatRemountShell = Feature("remount_shell")
|
||||
FeatSendrecvV2 = Feature("sendrecv_v2")
|
||||
FeatAbbExec = Feature("abb_exec")
|
||||
FeatFixedPushMkdir = Feature("fixed_push_mkdir")
|
||||
FeatFixedPushSymlinkTimestamp = Feature("fixed_push_symlink_timestamp")
|
||||
FeatAbb = Feature("abb")
|
||||
FeatShellV2 = Feature("shell_v2")
|
||||
FeatCmd = Feature("cmd")
|
||||
FeatLsV2 = Feature("ls_v2")
|
||||
FeatApex = Feature("apex")
|
||||
FeatStatV2 = Feature("stat_v2")
|
||||
)
|
||||
|
||||
func (fs Features) HasFeature(name Feature) bool {
|
||||
_, has := fs[name]
|
||||
return has
|
||||
}
|
||||
@@ -250,5 +250,6 @@ func (sync syncTransport) Close() (err error) {
|
||||
if sync.sock == nil {
|
||||
return nil
|
||||
}
|
||||
_ = DisableTimeWait(sync.sock.(*net.TCPConn))
|
||||
return sync.sock.Close()
|
||||
}
|
||||
|
||||
@@ -37,6 +37,14 @@ func (t transport) Send(command string) (err error) {
|
||||
return _send(t.sock, []byte(msg))
|
||||
}
|
||||
|
||||
func (t transport) SendBytes(b []byte) (err error) {
|
||||
return _send(t.sock, b)
|
||||
}
|
||||
|
||||
func (t transport) Conn() net.Conn {
|
||||
return t.sock
|
||||
}
|
||||
|
||||
func (t transport) VerifyResponse() (err error) {
|
||||
var status string
|
||||
if status, err = t.ReadStringN(4); err != nil {
|
||||
@@ -103,6 +111,7 @@ func (t transport) Close() (err error) {
|
||||
if t.sock == nil {
|
||||
return nil
|
||||
}
|
||||
_ = DisableTimeWait(t.sock.(*net.TCPConn))
|
||||
return t.sock.Close()
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ func Test_transport_VerifyResponse(t *testing.T) {
|
||||
}
|
||||
defer transport.Close()
|
||||
|
||||
// err = transport.Send("host:123version")
|
||||
err = transport.Send("host:version")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
9
hrp/pkg/gadb/utils.go
Normal file
9
hrp/pkg/gadb/utils.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package gadb
|
||||
|
||||
import (
|
||||
"net"
|
||||
)
|
||||
|
||||
func DisableTimeWait(conn *net.TCPConn) error {
|
||||
return conn.SetLinger(0)
|
||||
}
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
type adbDriver struct {
|
||||
Driver
|
||||
|
||||
adbClient gadb.Device
|
||||
adbClient *gadb.Device
|
||||
logcat *AdbLogcat
|
||||
}
|
||||
|
||||
@@ -153,11 +153,11 @@ func (ad *adbDriver) PressKeyCode(keyCode KeyCode, metaState KeyMeta) (err error
|
||||
return
|
||||
}
|
||||
|
||||
func (ad *adbDriver) AppLaunch(bundleId string) (err error) {
|
||||
func (ad *adbDriver) AppLaunch(packageName string) (err error) {
|
||||
// 不指定 Activity 名称启动(启动主 Activity)
|
||||
// adb shell monkey -p <packagename> -c android.intent.category.LAUNCHER 1
|
||||
sOutput, err := ad.adbClient.RunShellCommand(
|
||||
"monkey", "-p", bundleId, "-c", "android.intent.category.LAUNCHER", "1",
|
||||
"monkey", "-p", packageName, "-c", "android.intent.category.LAUNCHER", "1",
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -165,14 +165,22 @@ func (ad *adbDriver) AppLaunch(bundleId string) (err error) {
|
||||
if strings.Contains(sOutput, "monkey aborted") {
|
||||
return fmt.Errorf("app launch: %s", strings.TrimSpace(sOutput))
|
||||
}
|
||||
ad.lastLaunchedPackageName = packageName
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ad *adbDriver) AppTerminate(bundleId string) (successful bool, err error) {
|
||||
func (ad *adbDriver) AppTerminate(packageName string) (successful bool, err error) {
|
||||
// 强制停止应用,停止 <packagename> 相关的进程
|
||||
// adb shell am force-stop <packagename>
|
||||
_, err = ad.adbClient.RunShellCommand("am", "force-stop", bundleId)
|
||||
return err == nil, err
|
||||
_, err = ad.adbClient.RunShellCommand("am", "force-stop", packageName)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if ad.lastLaunchedPackageName == packageName {
|
||||
ad.lastLaunchedPackageName = "" // reset last launched package name
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (ad *adbDriver) Tap(x, y int, options ...DataOption) error {
|
||||
@@ -180,6 +188,13 @@ func (ad *adbDriver) Tap(x, y int, options ...DataOption) error {
|
||||
}
|
||||
|
||||
func (ad *adbDriver) TapFloat(x, y float64, options ...DataOption) (err error) {
|
||||
dataOptions := NewDataOptions(options...)
|
||||
|
||||
if len(dataOptions.Offset) == 2 {
|
||||
x += float64(dataOptions.Offset[0])
|
||||
y += float64(dataOptions.Offset[1])
|
||||
}
|
||||
|
||||
// adb shell input tap x y
|
||||
_, err = ad.adbClient.RunShellCommand(
|
||||
"input", "tap", fmt.Sprintf("%.1f", x), fmt.Sprintf("%.1f", y))
|
||||
@@ -273,9 +288,7 @@ func (ad *adbDriver) SetRotation(rotation Rotation) (err error) {
|
||||
|
||||
func (ad *adbDriver) Screenshot() (raw *bytes.Buffer, err error) {
|
||||
// adb shell screencap -p
|
||||
resp, err := ad.adbClient.RunShellCommandWithBytes(
|
||||
"screencap", "-p",
|
||||
)
|
||||
resp, err := ad.adbClient.ScreenCap()
|
||||
if err == nil {
|
||||
return bytes.NewBuffer(resp), nil
|
||||
}
|
||||
@@ -314,6 +327,13 @@ func (ad *adbDriver) IsHealthy() (healthy bool, err error) {
|
||||
|
||||
func (ad *adbDriver) StartCaptureLog(identifier ...string) (err error) {
|
||||
log.Info().Msg("start adb log recording")
|
||||
|
||||
// clear logcat
|
||||
if _, err = ad.adbClient.RunShellCommand("logcat", "-c"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// start logcat
|
||||
err = ad.logcat.CatchLogcat()
|
||||
if err != nil {
|
||||
err = errors.Wrap(code.AndroidCaptureLogError,
|
||||
@@ -335,3 +355,35 @@ func (ad *adbDriver) StopCaptureLog() (result interface{}, err error) {
|
||||
content := ad.logcat.logBuffer.String()
|
||||
return ConvertPoints(content), nil
|
||||
}
|
||||
|
||||
func (ad *adbDriver) GetLastLaunchedApp() (packageName string) {
|
||||
return ad.lastLaunchedPackageName
|
||||
}
|
||||
|
||||
func (ad *adbDriver) IsAppInForeground(packageName string) (bool, error) {
|
||||
if packageName == "" {
|
||||
return false, errors.New("package name is not given")
|
||||
}
|
||||
|
||||
// adb shell dumpsys activity activities | grep mResumedActivity
|
||||
output, err := ad.adbClient.RunShellCommand("dumpsys", "activity", "activities")
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to dumpsys activities")
|
||||
return false, err
|
||||
}
|
||||
|
||||
lines := strings.Split(string(output), "\n")
|
||||
isInForeground := false
|
||||
|
||||
for _, line := range lines {
|
||||
trimmedLine := strings.TrimSpace(line)
|
||||
if strings.HasPrefix(trimmedLine, "mResumedActivity:") {
|
||||
if strings.Contains(trimmedLine, packageName) {
|
||||
isInForeground = true
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return isInForeground, nil
|
||||
}
|
||||
|
||||
@@ -81,15 +81,6 @@ func GetAndroidDeviceOptions(dev *AndroidDevice) (deviceOptions []AndroidDeviceO
|
||||
// uiautomator2 server must be started before
|
||||
// adb shell am instrument -w io.appium.uiautomator2.server.test/androidx.test.runner.AndroidJUnitRunner
|
||||
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))
|
||||
} else if len(deviceList) == 0 {
|
||||
return nil, errors.Wrap(code.AndroidDeviceConnectionError,
|
||||
"not attached device found")
|
||||
}
|
||||
|
||||
device = &AndroidDevice{
|
||||
UIA2IP: UIA2ServerHost,
|
||||
UIA2Port: UIA2ServerPort,
|
||||
@@ -98,34 +89,56 @@ func NewAndroidDevice(options ...AndroidDeviceOption) (device *AndroidDevice, er
|
||||
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(device.SerialNumber)
|
||||
return device, nil
|
||||
deviceList, err := GetAndroidDevices(device.SerialNumber)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(code.AndroidDeviceConnectionError, err.Error())
|
||||
}
|
||||
|
||||
return nil, errors.Wrap(code.AndroidDeviceConnectionError,
|
||||
fmt.Sprintf("device %s not found", device.SerialNumber))
|
||||
dev := deviceList[0]
|
||||
device.SerialNumber = dev.Serial()
|
||||
device.d = dev
|
||||
device.logcat = NewAdbLogcat(device.SerialNumber)
|
||||
|
||||
log.Info().Str("serial", device.SerialNumber).Msg("select android device")
|
||||
return device, nil
|
||||
}
|
||||
|
||||
func DeviceList() (devices []gadb.Device, err error) {
|
||||
func GetAndroidDevices(serial ...string) (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()
|
||||
if devices, err = adbClient.DeviceList(); err != nil {
|
||||
return nil, errors.Wrap(code.AndroidDeviceConnectionError,
|
||||
fmt.Sprintf("list android devices failed: %v", err))
|
||||
}
|
||||
|
||||
var deviceList []*gadb.Device
|
||||
// filter by serial
|
||||
for _, d := range devices {
|
||||
for _, s := range serial {
|
||||
if s != "" && s != d.Serial() {
|
||||
continue
|
||||
}
|
||||
deviceList = append(deviceList, d)
|
||||
}
|
||||
}
|
||||
|
||||
if len(deviceList) == 0 {
|
||||
var err error
|
||||
if serial == nil || (len(serial) == 1 && serial[0] == "") {
|
||||
err = fmt.Errorf("no android device found")
|
||||
} else {
|
||||
err = fmt.Errorf("no android device found for serial %v", serial)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return deviceList, nil
|
||||
}
|
||||
|
||||
type AndroidDevice struct {
|
||||
d gadb.Device
|
||||
d *gadb.Device
|
||||
logcat *AdbLogcat
|
||||
SerialNumber string `json:"serial,omitempty" yaml:"serial,omitempty"`
|
||||
UIA2 bool `json:"uia2,omitempty" yaml:"uia2,omitempty"` // use uiautomator2
|
||||
@@ -138,6 +151,10 @@ func (dev *AndroidDevice) UUID() string {
|
||||
return dev.SerialNumber
|
||||
}
|
||||
|
||||
func (dev *AndroidDevice) LogEnabled() bool {
|
||||
return dev.LogOn
|
||||
}
|
||||
|
||||
func (dev *AndroidDevice) NewDriver(capabilities Capabilities) (driverExt *DriverExt, err error) {
|
||||
var driver WebDriver
|
||||
if dev.UIA2 {
|
||||
@@ -311,12 +328,13 @@ func (l *AdbLogcat) CatchLogcat() (err error) {
|
||||
}
|
||||
|
||||
// clear logcat
|
||||
if err = myexec.RunCommand("adb", "-s", l.serial, "logcat", "-c"); err != nil {
|
||||
if err = myexec.RunCommand("adb", "-s", l.serial, "shell", "logcat", "-c"); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// start logcat
|
||||
l.cmd = myexec.Command("adb", "-s", l.serial, "logcat", "-v", "time", "-s", "iesqaMonitor:V")
|
||||
l.cmd = myexec.Command("adb", "-s", l.serial,
|
||||
"logcat", "--format", "time", "-s", "iesqaMonitor:V")
|
||||
l.cmd.Stderr = l.logBuffer
|
||||
l.cmd.Stdout = l.logBuffer
|
||||
if err = l.cmd.Start(); err != nil {
|
||||
@@ -325,7 +343,7 @@ func (l *AdbLogcat) CatchLogcat() (err error) {
|
||||
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))
|
||||
log.Error().Err(e).Msg("kill logcat process failed")
|
||||
}
|
||||
l.done <- struct{}{}
|
||||
}()
|
||||
|
||||
@@ -324,7 +324,7 @@ func Test_getFreePort(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDeviceList(t *testing.T) {
|
||||
devices, err := DeviceList()
|
||||
devices, err := GetAndroidDevices()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -353,6 +353,34 @@ func TestDriver_AppLaunch(t *testing.T) {
|
||||
t.Log(ioutil.WriteFile("s1.png", raw.Bytes(), 0o600))
|
||||
}
|
||||
|
||||
func TestDriver_IsAppInForeground(t *testing.T) {
|
||||
device, _ := NewAndroidDevice()
|
||||
driver, err := device.NewDriver(nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = driver.Driver.AppLaunch("com.android.settings")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
yes, err := driver.Driver.IsAppInForeground(driver.Driver.GetLastLaunchedApp())
|
||||
if err != nil || !yes {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = driver.Driver.AppTerminate("com.android.settings")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
yes, err = driver.Driver.IsAppInForeground("com.android.settings")
|
||||
if err != nil || yes {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDriver_KeepAlive(t *testing.T) {
|
||||
device, _ := NewAndroidDevice()
|
||||
driver, err := device.NewDriver(nil)
|
||||
|
||||
@@ -20,6 +20,8 @@ type Driver struct {
|
||||
urlPrefix *url.URL
|
||||
sessionId string
|
||||
client *http.Client
|
||||
// cache the last launched package name
|
||||
lastLaunchedPackageName string
|
||||
}
|
||||
|
||||
func (wd *Driver) concatURL(u *url.URL, elem ...string) string {
|
||||
|
||||
@@ -5,8 +5,10 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/gif"
|
||||
"image/jpeg"
|
||||
"image/png"
|
||||
"math/rand"
|
||||
"mime"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
@@ -32,16 +34,22 @@ const (
|
||||
AppStop MobileMethod = "app_stop"
|
||||
CtlScreenShot MobileMethod = "screenshot"
|
||||
CtlSleep MobileMethod = "sleep"
|
||||
CtlSleepRandom MobileMethod = "sleep_random"
|
||||
CtlStartCamera MobileMethod = "camera_start" // alias for app_launch camera
|
||||
CtlStopCamera MobileMethod = "camera_stop" // alias for app_terminate camera
|
||||
RecordStart MobileMethod = "record_start"
|
||||
RecordStop MobileMethod = "record_stop"
|
||||
|
||||
// UI validation
|
||||
SelectorName string = "ui_name"
|
||||
SelectorLabel string = "ui_label"
|
||||
SelectorOCR string = "ui_ocr"
|
||||
SelectorImage string = "ui_image"
|
||||
// selectors
|
||||
SelectorName string = "ui_name"
|
||||
SelectorLabel string = "ui_label"
|
||||
SelectorOCR string = "ui_ocr"
|
||||
SelectorImage string = "ui_image"
|
||||
SelectorForegroundApp string = "ui_foreground_app"
|
||||
// assertions
|
||||
AssertionEqual string = "equal"
|
||||
AssertionNotEqual string = "not_equal"
|
||||
AssertionExists string = "exists"
|
||||
AssertionNotExists string = "not_exists"
|
||||
|
||||
@@ -209,7 +217,7 @@ type DriverExt struct {
|
||||
doneMjpegStream chan bool
|
||||
scale float64
|
||||
ocrService OCRService // used to get text from image
|
||||
ScreenShots []string // save screenshots path
|
||||
screenShots []string // cache screenshot paths
|
||||
|
||||
CVArgs
|
||||
}
|
||||
@@ -245,7 +253,9 @@ func NewDriverExt(device Device, driver WebDriver) (dExt *DriverExt, err error)
|
||||
return dExt, nil
|
||||
}
|
||||
|
||||
func (dExt *DriverExt) takeScreenShot() (raw *bytes.Buffer, err error) {
|
||||
// TakeScreenShot takes screenshot and saves image file to $CWD/screenshots/ folder
|
||||
// if fileName is empty, it will not save image file and only return raw image data
|
||||
func (dExt *DriverExt) TakeScreenShot(fileName ...string) (raw *bytes.Buffer, err error) {
|
||||
// wait for action done
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
@@ -255,15 +265,34 @@ func (dExt *DriverExt) takeScreenShot() (raw *bytes.Buffer, err error) {
|
||||
return dExt.frame, nil
|
||||
}
|
||||
if raw, err = dExt.Driver.Screenshot(); err != nil {
|
||||
log.Error().Err(err).Msg("takeScreenShot failed")
|
||||
log.Error().Err(err).Msg("capture screenshot data failed")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// save screenshot to file
|
||||
if len(fileName) > 0 && fileName[0] != "" {
|
||||
path := filepath.Join(env.ScreenShotsPath, fileName[0])
|
||||
path, err := dExt.saveScreenShot(raw, path)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("save screenshot file failed")
|
||||
return nil, err
|
||||
}
|
||||
dExt.screenShots = append(dExt.screenShots, path)
|
||||
log.Info().Str("path", path).Msg("save screenshot file success")
|
||||
}
|
||||
|
||||
return raw, nil
|
||||
}
|
||||
|
||||
// saveScreenShot saves image file with file name
|
||||
func saveScreenShot(raw *bytes.Buffer, fileName string) (string, error) {
|
||||
img, format, err := image.Decode(raw)
|
||||
func (dExt *DriverExt) saveScreenShot(raw *bytes.Buffer, fileName string) (string, error) {
|
||||
// notice: screenshot data is a stream, so we need to copy it to a new buffer
|
||||
copiedBuffer := &bytes.Buffer{}
|
||||
if _, err := copiedBuffer.Write(raw.Bytes()); err != nil {
|
||||
log.Error().Err(err).Msg("copy screenshot buffer failed")
|
||||
}
|
||||
|
||||
img, format, err := image.Decode(copiedBuffer)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "decode screenshot image failed")
|
||||
}
|
||||
@@ -282,6 +311,8 @@ func saveScreenShot(raw *bytes.Buffer, fileName string) (string, error) {
|
||||
err = png.Encode(file, img)
|
||||
case "jpeg":
|
||||
err = jpeg.Encode(file, img, nil)
|
||||
case "gif":
|
||||
err = gif.Encode(file, img, nil)
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported image format: %s", format)
|
||||
}
|
||||
@@ -292,19 +323,11 @@ func saveScreenShot(raw *bytes.Buffer, fileName string) (string, error) {
|
||||
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")
|
||||
}
|
||||
|
||||
fileName = filepath.Join(env.ScreenShotsPath, fileName)
|
||||
path, err := saveScreenShot(raw, fileName)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "save screenshot failed")
|
||||
}
|
||||
return path, nil
|
||||
func (dExt *DriverExt) GetScreenShots() []string {
|
||||
defer func() {
|
||||
dExt.screenShots = nil
|
||||
}()
|
||||
return dExt.screenShots
|
||||
}
|
||||
|
||||
// isPathExists returns true if path exists, whether path is file or dir
|
||||
@@ -315,6 +338,10 @@ func isPathExists(path string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func init() {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
}
|
||||
|
||||
func (dExt *DriverExt) FindUIRectInUIKit(search string, options ...DataOption) (x, y, width, height float64, err error) {
|
||||
// click on text, using OCR
|
||||
if !isPathExists(search) {
|
||||
@@ -340,8 +367,31 @@ func (dExt *DriverExt) IsImageExist(text string) bool {
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (dExt *DriverExt) IsAppInForeground(packageName string) bool {
|
||||
// check if app is in foreground
|
||||
yes, err := dExt.Driver.IsAppInForeground(packageName)
|
||||
if !yes || err != nil {
|
||||
log.Info().Str("packageName", packageName).Msg("app is not in foreground")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
var errActionNotImplemented = errors.New("UI action not implemented")
|
||||
|
||||
func convertToFloat64(val interface{}) (float64, error) {
|
||||
switch v := val.(type) {
|
||||
case float64:
|
||||
return v, nil
|
||||
case int:
|
||||
return float64(v), nil
|
||||
case int64:
|
||||
return float64(v), nil
|
||||
default:
|
||||
return 0, fmt.Errorf("invalid type for conversion to float64: %T, value: %+v", val, val)
|
||||
}
|
||||
}
|
||||
|
||||
func (dExt *DriverExt) DoAction(action MobileAction) error {
|
||||
log.Info().Str("method", string(action.Method)).Interface("params", action.Params).Msg("start UI action")
|
||||
|
||||
@@ -602,16 +652,59 @@ func (dExt *DriverExt) DoAction(action MobileAction) error {
|
||||
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("screenshot_%d",
|
||||
time.Now().Unix()))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "take screenshot failed")
|
||||
case CtlSleepRandom:
|
||||
params, ok := action.Params.([]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid sleep random params: %v(%T)", action.Params, action.Params)
|
||||
}
|
||||
log.Info().Str("path", screenshotPath).Msg("take screenshot")
|
||||
dExt.ScreenShots = append(dExt.ScreenShots, screenshotPath)
|
||||
// append default weight 1
|
||||
if len(params) == 2 {
|
||||
params = append(params, 1.0)
|
||||
}
|
||||
|
||||
var sections []struct {
|
||||
min, max, weight float64
|
||||
}
|
||||
totalProb := 0.0
|
||||
for i := 0; i+3 <= len(params); i += 3 {
|
||||
min, err := convertToFloat64(params[i])
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "invalid minimum time: %v", params[i])
|
||||
}
|
||||
max, err := convertToFloat64(params[i+1])
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "invalid maximum time: %v", params[i+1])
|
||||
}
|
||||
weight, err := convertToFloat64(params[i+2])
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "invalid weight value: %v", params[i+2])
|
||||
}
|
||||
totalProb += weight
|
||||
sections = append(sections,
|
||||
struct{ min, max, weight float64 }{min, max, weight},
|
||||
)
|
||||
}
|
||||
|
||||
if totalProb == 0 {
|
||||
log.Warn().Msg("total weight is 0, skip sleep")
|
||||
return nil
|
||||
}
|
||||
|
||||
r := rand.Float64()
|
||||
accProb := 0.0
|
||||
for _, s := range sections {
|
||||
accProb += s.weight / totalProb
|
||||
if r < accProb {
|
||||
n := s.min + rand.Float64()*(s.max-s.min)
|
||||
log.Info().Float64("duration", n).Msg("sleep random seconds")
|
||||
time.Sleep(time.Duration(n*1000) * time.Millisecond)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
case CtlScreenShot:
|
||||
// take screenshot
|
||||
log.Info().Msg("take screenshot for current screen")
|
||||
_, err := dExt.TakeScreenShot(builtin.GenNameWithTimestamp("step_%d_screenshot"))
|
||||
return err
|
||||
case CtlStartCamera:
|
||||
return dExt.Driver.StartCamera()
|
||||
@@ -629,18 +722,20 @@ func (dExt *DriverExt) getAbsScope(x1, y1, x2, y2 float64) (int, int, int, int)
|
||||
}
|
||||
|
||||
func (dExt *DriverExt) DoValidation(check, assert, expected string, message ...string) bool {
|
||||
var exists bool
|
||||
if assert == AssertionExists {
|
||||
exists = true
|
||||
var exp bool
|
||||
if assert == AssertionExists || assert == AssertionEqual {
|
||||
exp = true
|
||||
} else {
|
||||
exists = false
|
||||
exp = false
|
||||
}
|
||||
var result bool
|
||||
switch check {
|
||||
case SelectorOCR:
|
||||
result = (dExt.IsOCRExist(expected) == exists)
|
||||
result = (dExt.IsOCRExist(expected) == exp)
|
||||
case SelectorImage:
|
||||
result = (dExt.IsImageExist(expected) == exists)
|
||||
result = (dExt.IsImageExist(expected) == exp)
|
||||
case SelectorForegroundApp:
|
||||
result = (dExt.IsAppInForeground(expected) == exp)
|
||||
}
|
||||
|
||||
if !result {
|
||||
|
||||
@@ -2,7 +2,6 @@ package uixt
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -437,7 +436,6 @@ type DataOptions struct {
|
||||
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
|
||||
ScreenShotFilename string // turn on screenshot and specify file name
|
||||
}
|
||||
|
||||
type DataOption func(data *DataOptions)
|
||||
@@ -514,16 +512,6 @@ func WithDataWaitTime(sec float64) DataOption {
|
||||
}
|
||||
}
|
||||
|
||||
func WithScreenShot(fileName ...string) DataOption {
|
||||
return func(data *DataOptions) {
|
||||
if len(fileName) > 0 {
|
||||
data.ScreenShotFilename = fileName[0]
|
||||
} else {
|
||||
data.ScreenShotFilename = fmt.Sprintf("screenshot_%d", time.Now().Unix())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func NewDataOptions(options ...DataOption) *DataOptions {
|
||||
dataOptions := &DataOptions{
|
||||
Data: make(map[string]interface{}),
|
||||
@@ -581,6 +569,7 @@ func NewData(data map[string]interface{}, options ...DataOption) map[string]inte
|
||||
// current implemeted device: IOSDevice, AndroidDevice
|
||||
type Device interface {
|
||||
UUID() string // ios udid or android serial
|
||||
LogEnabled() bool
|
||||
NewDriver(capabilities Capabilities) (driverExt *DriverExt, err error)
|
||||
|
||||
StartPerf() error
|
||||
@@ -626,10 +615,14 @@ type WebDriver interface {
|
||||
|
||||
// AppLaunch Launch an application with given bundle identifier in scope of current session.
|
||||
// !This method is only available since Xcode9 SDK
|
||||
AppLaunch(bundleId string) error
|
||||
// AppTerminate Terminate an application with the given bundle id.
|
||||
AppLaunch(packageName string) error
|
||||
// AppTerminate Terminate an application with the given pacakge name.
|
||||
// Either `true` if the app has been successfully terminated or `false` if it was not running
|
||||
AppTerminate(bundleId string) (bool, error)
|
||||
AppTerminate(packageName string) (bool, error)
|
||||
// GetLastLaunchedApp returns the package name of the last launched app
|
||||
GetLastLaunchedApp() string
|
||||
// IsAppInForeground returns true if the given package is in foreground
|
||||
IsAppInForeground(packageName string) (bool, error)
|
||||
|
||||
// StartCamera Starts a new camera for recording
|
||||
StartCamera() error
|
||||
|
||||
@@ -141,7 +141,7 @@ func WithIOSPcapOptions(options ...gidevice.PcapOption) IOSDeviceOption {
|
||||
}
|
||||
}
|
||||
|
||||
func IOSDevices(udid ...string) (devices []gidevice.Device, err error) {
|
||||
func GetIOSDevices(udid ...string) (devices []gidevice.Device, err error) {
|
||||
var usbmux gidevice.Usbmux
|
||||
if usbmux, err = gidevice.NewUsbmux(); err != nil {
|
||||
return nil, errors.Wrap(code.IOSDeviceConnectionError,
|
||||
@@ -168,6 +168,15 @@ func IOSDevices(udid ...string) (devices []gidevice.Device, err error) {
|
||||
}
|
||||
}
|
||||
|
||||
if len(deviceList) == 0 {
|
||||
var err error
|
||||
if udid == nil || (len(udid) == 1 && udid[0] == "") {
|
||||
err = fmt.Errorf("no ios device found")
|
||||
} else {
|
||||
err = fmt.Errorf("no ios device found for udid %v", udid)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return deviceList, nil
|
||||
}
|
||||
|
||||
@@ -223,31 +232,27 @@ func NewIOSDevice(options ...IOSDeviceOption) (device *IOSDevice, err error) {
|
||||
option(device)
|
||||
}
|
||||
|
||||
deviceList, err := IOSDevices(device.UDID)
|
||||
deviceList, err := GetIOSDevices(device.UDID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, errors.Wrap(code.IOSDeviceConnectionError, err.Error())
|
||||
}
|
||||
|
||||
for _, dev := range deviceList {
|
||||
udid := dev.Properties().SerialNumber
|
||||
device.UDID = udid
|
||||
device.d = dev
|
||||
dev := deviceList[0]
|
||||
udid := dev.Properties().SerialNumber
|
||||
device.UDID = udid
|
||||
device.d = dev
|
||||
|
||||
// run xctest if XCTestBundleID is set
|
||||
if device.XCTestBundleID != "" {
|
||||
_, err = device.RunXCTest(device.XCTestBundleID)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("udid", udid).Msg("failed to init XCTest")
|
||||
continue
|
||||
}
|
||||
// run xctest if XCTestBundleID is set
|
||||
if device.XCTestBundleID != "" {
|
||||
_, err = device.RunXCTest(device.XCTestBundleID)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("udid", udid).Msg("failed to init XCTest")
|
||||
return
|
||||
}
|
||||
|
||||
log.Info().Str("udid", device.UDID).Msg("select device")
|
||||
return device, nil
|
||||
}
|
||||
|
||||
return nil, errors.Wrap(code.IOSDeviceConnectionError,
|
||||
fmt.Sprintf("device %s not found", device.UDID))
|
||||
log.Info().Str("udid", device.UDID).Msg("select ios device")
|
||||
return device, nil
|
||||
}
|
||||
|
||||
type IOSDevice struct {
|
||||
@@ -281,6 +286,10 @@ func (dev *IOSDevice) UUID() string {
|
||||
return dev.UDID
|
||||
}
|
||||
|
||||
func (dev *IOSDevice) LogEnabled() bool {
|
||||
return dev.LogOn
|
||||
}
|
||||
|
||||
func (dev *IOSDevice) NewDriver(capabilities Capabilities) (driverExt *DriverExt, err error) {
|
||||
// init WDA driver
|
||||
if capabilities == nil {
|
||||
|
||||
@@ -308,6 +308,9 @@ func (wd *wdaDriver) AppLaunch(bundleId string) (err error) {
|
||||
data := make(map[string]interface{})
|
||||
data["bundleId"] = bundleId
|
||||
_, err = wd.httpPOST(data, "/session", wd.sessionId, "/wda/apps/launch")
|
||||
if err == nil {
|
||||
wd.lastLaunchedPackageName = bundleId
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -328,6 +331,9 @@ func (wd *wdaDriver) AppTerminate(bundleId string) (successful bool, err error)
|
||||
if successful, err = rawResp.valueConvertToBool(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if wd.lastLaunchedPackageName == bundleId {
|
||||
wd.lastLaunchedPackageName = "" // reset last launched package name
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -348,6 +354,14 @@ func (wd *wdaDriver) AppDeactivate(second float64) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (wd *wdaDriver) GetLastLaunchedApp() (packageName string) {
|
||||
return wd.lastLaunchedPackageName
|
||||
}
|
||||
|
||||
func (wd *wdaDriver) IsAppInForeground(packageName string) (bool, error) {
|
||||
return false, errors.New("not implemented")
|
||||
}
|
||||
|
||||
func (wd *wdaDriver) Tap(x, y int, options ...DataOption) error {
|
||||
return wd.TapFloat(float64(x), float64(y), options...)
|
||||
}
|
||||
|
||||
@@ -93,16 +93,19 @@ func (s *veDEMOCRService) getOCRResult(imageBuf *bytes.Buffer) ([]OCRResult, err
|
||||
// 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)
|
||||
}
|
||||
if err == nil && resp.StatusCode == http.StatusOK {
|
||||
log.Debug().
|
||||
Str("X-TT-LOGID", logID).
|
||||
Int("imageBufSize", size).
|
||||
Msg("request OCR service success")
|
||||
break
|
||||
}
|
||||
log.Error().Err(err).
|
||||
Str("logID", logID).
|
||||
Str("X-TT-LOGID", logID).
|
||||
Int("imageBufSize", size).
|
||||
Msgf("request OCR service failed, retry %d", i)
|
||||
time.Sleep(1 * time.Second)
|
||||
@@ -172,14 +175,6 @@ func (s *veDEMOCRService) GetTexts(imageBuf *bytes.Buffer, options ...DataOption
|
||||
|
||||
dataOptions := NewDataOptions(options...)
|
||||
|
||||
if dataOptions.ScreenShotFilename != "" {
|
||||
path, err := saveScreenShot(imageBuf, dataOptions.ScreenShotFilename)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "save screenshot failed")
|
||||
}
|
||||
log.Debug().Str("path", path).Msg("save screenshot")
|
||||
}
|
||||
|
||||
for _, ocrResult := range ocrResults {
|
||||
rect := image.Rectangle{
|
||||
// ocrResult.Points 顺序:左上 -> 右上 -> 右下 -> 左下
|
||||
@@ -310,8 +305,7 @@ type OCRService interface {
|
||||
|
||||
func (dExt *DriverExt) GetTextsByOCR(options ...DataOption) (texts OCRTexts, err error) {
|
||||
var bufSource *bytes.Buffer
|
||||
if bufSource, err = dExt.takeScreenShot(); err != nil {
|
||||
err = fmt.Errorf("takeScreenShot error: %v", err)
|
||||
if bufSource, err = dExt.TakeScreenShot(builtin.GenNameWithTimestamp("step_%d_ocr")); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -326,8 +320,7 @@ func (dExt *DriverExt) GetTextsByOCR(options ...DataOption) (texts OCRTexts, err
|
||||
|
||||
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)
|
||||
if bufSource, err = dExt.TakeScreenShot(builtin.GenNameWithTimestamp("step_%d_ocr")); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -345,8 +338,7 @@ func (dExt *DriverExt) FindTextByOCR(ocrText string, options ...DataOption) (x,
|
||||
|
||||
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)
|
||||
if bufSource, err = dExt.TakeScreenShot(builtin.GenNameWithTimestamp("step_%d_ocr")); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ import (
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"gocv.io/x/gocv"
|
||||
|
||||
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -101,7 +103,7 @@ func (dExt *DriverExt) FindAllImageRect(search string) (rects []image.Rectangle,
|
||||
if bufSearch, err = getBufFromDisk(search); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if bufSource, err = dExt.takeScreenShot(); err != nil {
|
||||
if bufSource, err = dExt.TakeScreenShot(builtin.GenNameWithTimestamp("step_%d_cv")); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -116,7 +118,7 @@ func (dExt *DriverExt) FindImageRectInUIKit(imagePath string, options ...DataOpt
|
||||
if bufSearch, err = getBufFromDisk(imagePath); err != nil {
|
||||
return 0, 0, 0, 0, err
|
||||
}
|
||||
if bufSource, err = dExt.takeScreenShot(); err != nil {
|
||||
if bufSource, err = dExt.TakeScreenShot(builtin.GenNameWithTimestamp("step_%d_cv")); err != nil {
|
||||
return 0, 0, 0, 0, err
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,10 @@ const (
|
||||
|
||||
const projectInfoFile = "proj.json" // used for ensuring root project
|
||||
|
||||
var pluginMap = sync.Map{} // used for reusing plugin instance
|
||||
var (
|
||||
pluginMap sync.Map // used for reusing plugin instance
|
||||
pluginMutex sync.RWMutex
|
||||
)
|
||||
|
||||
func initPlugin(path, venv string, logOn bool) (plugin funplugin.IPlugin, err error) {
|
||||
// plugin file not found
|
||||
@@ -42,6 +45,9 @@ func initPlugin(path, venv string, logOn bool) (plugin funplugin.IPlugin, err er
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
pluginMutex.Lock()
|
||||
defer pluginMutex.Unlock()
|
||||
|
||||
// reuse plugin instance if it already initialized
|
||||
if p, ok := pluginMap.Load(pluginPath); ok {
|
||||
return p.(funplugin.IPlugin), nil
|
||||
|
||||
121
hrp/runner.go
121
hrp/runner.go
@@ -386,7 +386,7 @@ func (r *CaseRunner) parseConfig() error {
|
||||
r.parsedConfig.WebSocketSetting.checkWebSocket()
|
||||
|
||||
// parse testcase config parameters
|
||||
parametersIterator, err := initParametersIterator(r.parsedConfig)
|
||||
parametersIterator, err := r.parser.initParametersIterator(r.parsedConfig)
|
||||
if err != nil {
|
||||
log.Error().Err(err).
|
||||
Interface("parameters", r.parsedConfig.Parameters).
|
||||
@@ -461,6 +461,7 @@ type SessionRunner struct {
|
||||
startTime time.Time // record start time of the testcase
|
||||
summary *TestCaseSummary // record test case summary
|
||||
wsConnMap map[string]*websocket.Conn // save all websocket connections
|
||||
inheritWsConnMap map[string]*websocket.Conn // inherit all websocket connections
|
||||
pongResponseChan chan string // channel used to receive pong response message
|
||||
closeResponseChan chan *wsCloseRespObject // channel used to receive close response message
|
||||
}
|
||||
@@ -472,22 +473,36 @@ func (r *SessionRunner) resetSession() {
|
||||
r.startTime = time.Now()
|
||||
r.summary = newSummary()
|
||||
r.wsConnMap = make(map[string]*websocket.Conn)
|
||||
r.inheritWsConnMap = make(map[string]*websocket.Conn)
|
||||
r.pongResponseChan = make(chan string, 1)
|
||||
r.closeResponseChan = make(chan *wsCloseRespObject, 1)
|
||||
}
|
||||
|
||||
func (r *SessionRunner) inheritConnection(src *SessionRunner) {
|
||||
log.Info().Msg("inherit session runner")
|
||||
r.inheritWsConnMap = make(map[string]*websocket.Conn, len(src.wsConnMap)+len(src.inheritWsConnMap))
|
||||
for k, v := range src.wsConnMap {
|
||||
r.inheritWsConnMap[k] = v
|
||||
}
|
||||
for k, v := range src.inheritWsConnMap {
|
||||
r.inheritWsConnMap[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
defer func() {
|
||||
// close session resource after all steps done or fast fail
|
||||
r.releaseResources()
|
||||
}()
|
||||
|
||||
// run step in sequential order
|
||||
for _, step := range r.caseRunner.testCase.TestSteps {
|
||||
// TODO: parse step struct
|
||||
@@ -524,17 +539,7 @@ func (r *SessionRunner) Start(givenVars map[string]interface{}) error {
|
||||
stepResult, err = step.Run(r)
|
||||
stepResult.Name = stepName + loopIndex
|
||||
|
||||
// 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
|
||||
r.updateSummary(stepResult)
|
||||
}
|
||||
|
||||
// update extracted variables
|
||||
@@ -559,23 +564,10 @@ func (r *SessionRunner) Start(givenVars map[string]interface{}) error {
|
||||
|
||||
// check if failfast
|
||||
if r.caseRunner.hrpRunner.failfast {
|
||||
return errors.New("abort running due to failfast setting")
|
||||
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
|
||||
}
|
||||
@@ -625,13 +617,16 @@ func (r *SessionRunner) GetSummary() (*TestCaseSummary, error) {
|
||||
|
||||
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,
|
||||
"uuid": uuid,
|
||||
}
|
||||
|
||||
if client.Device.LogEnabled() {
|
||||
log, err := client.Driver.StopCaptureLog()
|
||||
if err != nil {
|
||||
return caseSummary, err
|
||||
}
|
||||
logs["content"] = log
|
||||
}
|
||||
|
||||
// stop performance monitor
|
||||
@@ -643,3 +638,59 @@ func (r *SessionRunner) GetSummary() (*TestCaseSummary, error) {
|
||||
|
||||
return caseSummary, nil
|
||||
}
|
||||
|
||||
// updateSummary updates summary of StepResult.
|
||||
func (r *SessionRunner) updateSummary(stepResult *StepResult) {
|
||||
switch stepResult.StepType {
|
||||
case stepTypeTestCase:
|
||||
// record requests of testcase step
|
||||
if records, ok := stepResult.Data.([]*StepResult); ok {
|
||||
for _, result := range records {
|
||||
r.addSingleStepResult(result)
|
||||
}
|
||||
} else {
|
||||
r.addSingleStepResult(stepResult)
|
||||
}
|
||||
default:
|
||||
r.addSingleStepResult(stepResult)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *SessionRunner) addSingleStepResult(stepResult *StepResult) {
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
// releaseResources releases resources used by session runner
|
||||
func (r *SessionRunner) releaseResources() {
|
||||
// close websocket connections
|
||||
for _, wsConn := range r.wsConnMap {
|
||||
if wsConn != nil {
|
||||
log.Info().Str("testcase", r.caseRunner.testCase.Config.Name).Msg("websocket disconnected")
|
||||
err := wsConn.Close()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("websocket disconnection failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *SessionRunner) getWsClient(url string) *websocket.Conn {
|
||||
if client, ok := r.wsConnMap[url]; ok {
|
||||
return client
|
||||
}
|
||||
|
||||
if client, ok := r.inheritWsConnMap[url]; ok {
|
||||
return client
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ func assertRunTestCases(t *testing.T) {
|
||||
refCase := TestCasePath(demoTestCaseWithPluginJSONPath)
|
||||
testcase1 := &TestCase{
|
||||
Config: NewConfig("TestCase1").
|
||||
SetBaseURL("http://httpbin.org"),
|
||||
SetBaseURL("https://httpbin.org"),
|
||||
TestSteps: []IStep{
|
||||
NewStep("testcase1-step1").
|
||||
GET("/headers").
|
||||
@@ -77,7 +77,7 @@ func assertRunTestCases(t *testing.T) {
|
||||
AssertEqual("headers.\"Content-Type\"", "application/json", "check http response Content-Type"),
|
||||
NewStep("testcase1-step3").CallRefCase(
|
||||
&TestCase{
|
||||
Config: NewConfig("testcase1-step3-ref-case").SetBaseURL("http://httpbin.org"),
|
||||
Config: NewConfig("testcase1-step3-ref-case").SetBaseURL("https://httpbin.org"),
|
||||
TestSteps: []IStep{
|
||||
NewStep("ip").
|
||||
GET("/ip").
|
||||
|
||||
@@ -2,11 +2,11 @@ package hrp
|
||||
|
||||
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"
|
||||
"github.com/httprunner/httprunner/v4/hrp/pkg/uixt"
|
||||
)
|
||||
@@ -281,6 +281,18 @@ func (s *StepMobile) Sleep(n float64) *StepMobile {
|
||||
return &StepMobile{step: s.step}
|
||||
}
|
||||
|
||||
// SleepRandom specify random sleeping seconds after last action
|
||||
// params have two different kinds:
|
||||
// 1. [min, max] : min and max are float64 time range boudaries
|
||||
// 2. [min1, max1, weight1, min2, max2, weight2, ...] : weight is the probability of the time range
|
||||
func (s *StepMobile) SleepRandom(params ...float64) *StepMobile {
|
||||
s.mobileStep().Actions = append(s.mobileStep().Actions, uixt.MobileAction{
|
||||
Method: uixt.CtlSleepRandom,
|
||||
Params: params,
|
||||
})
|
||||
return &StepMobile{step: s.step}
|
||||
}
|
||||
|
||||
func (s *StepMobile) ScreenShot() *StepMobile {
|
||||
s.mobileStep().Actions = append(s.mobileStep().Actions, uixt.MobileAction{
|
||||
Method: uixt.CtlScreenShot,
|
||||
@@ -456,6 +468,36 @@ func (s *StepMobileUIValidation) AssertImageNotExists(expectedImagePath string,
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *StepMobileUIValidation) AssertAppInForeground(packageName string, msg ...string) *StepMobileUIValidation {
|
||||
v := Validator{
|
||||
Check: uixt.SelectorForegroundApp,
|
||||
Assert: uixt.AssertionEqual,
|
||||
Expect: packageName,
|
||||
}
|
||||
if len(msg) > 0 {
|
||||
v.Message = msg[0]
|
||||
} else {
|
||||
v.Message = fmt.Sprintf("app [%s] should be in foreground", packageName)
|
||||
}
|
||||
s.step.Validators = append(s.step.Validators, v)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *StepMobileUIValidation) AssertAppNotInForeground(packageName string, msg ...string) *StepMobileUIValidation {
|
||||
v := Validator{
|
||||
Check: uixt.SelectorForegroundApp,
|
||||
Assert: uixt.AssertionNotEqual,
|
||||
Expect: packageName,
|
||||
}
|
||||
if len(msg) > 0 {
|
||||
v.Message = msg[0]
|
||||
} else {
|
||||
v.Message = fmt.Sprintf("app [%s] should not be in foreground", packageName)
|
||||
}
|
||||
s.step.Validators = append(s.step.Validators, v)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *StepMobileUIValidation) Name() string {
|
||||
return s.step.Name
|
||||
}
|
||||
@@ -530,7 +572,6 @@ func runStepMobileUI(s *SessionRunner, step *TStep) (stepResult *StepResult, err
|
||||
Success: false,
|
||||
ContentSize: 0,
|
||||
}
|
||||
screenshots := make([]string, 0)
|
||||
|
||||
// merge step variables with session variables
|
||||
stepVariables, err := s.ParseStepVariables(step.Variables)
|
||||
@@ -549,11 +590,25 @@ func runStepMobileUI(s *SessionRunner, step *TStep) (stepResult *StepResult, err
|
||||
attachments := make(map[string]interface{})
|
||||
if err != nil {
|
||||
attachments["error"] = err.Error()
|
||||
|
||||
// check if app is in foreground
|
||||
packageName := uiDriver.Driver.GetLastLaunchedApp()
|
||||
yes, err2 := uiDriver.Driver.IsAppInForeground(packageName)
|
||||
if packageName != "" && (!yes || err2 != nil) {
|
||||
log.Error().Err(err2).Str("packageName", packageName).Msg("app is not in foreground")
|
||||
err = errors.Wrap(code.MobileUIAppNotInForegroundError, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// take screenshot after each step
|
||||
_, err := uiDriver.TakeScreenShot(
|
||||
builtin.GenNameWithTimestamp("step_%d_") + step.Name)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("step", step.Name).Msg("take screenshot failed on step finished")
|
||||
}
|
||||
|
||||
// save attachments
|
||||
screenshots = append(screenshots, uiDriver.ScreenShots...)
|
||||
attachments["screenshots"] = screenshots
|
||||
attachments["screenshots"] = uiDriver.GetScreenShots()
|
||||
stepResult.Attachments = attachments
|
||||
}()
|
||||
|
||||
@@ -587,16 +642,6 @@ func runStepMobileUI(s *SessionRunner, step *TStep) (stepResult *StepResult, err
|
||||
}
|
||||
}
|
||||
|
||||
// take snapshot
|
||||
screenshotPath, err := uiDriver.ScreenShot(
|
||||
fmt.Sprintf("validate_%d", 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 {
|
||||
|
||||
@@ -187,6 +187,9 @@ func (r *requestBuilder) prepareUrlParams(stepVariables map[string]interface{})
|
||||
r.req.URL = u
|
||||
r.req.Host = u.Host
|
||||
|
||||
// update url
|
||||
r.requestMap["url"] = u.String()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -337,13 +340,34 @@ func runStepRequest(r *SessionRunner, step *TStep) (stepResult *StepResult, err
|
||||
// add request object to step variables, could be used in setup hooks
|
||||
stepVariables["hrp_step_name"] = step.Name
|
||||
stepVariables["hrp_step_request"] = rb.requestMap
|
||||
stepVariables["request"] = rb.requestMap
|
||||
|
||||
// deal with setup hooks
|
||||
for _, setupHook := range step.SetupHooks {
|
||||
_, err = parser.Parse(setupHook, stepVariables)
|
||||
req, err := parser.Parse(setupHook, stepVariables)
|
||||
if err != nil {
|
||||
return stepResult, errors.Wrap(err, "run setup hooks failed")
|
||||
}
|
||||
reqMap, ok := req.(map[string]interface{})
|
||||
if ok && reqMap != nil {
|
||||
rb.requestMap = reqMap
|
||||
stepVariables["request"] = reqMap
|
||||
}
|
||||
}
|
||||
if len(step.SetupHooks) > 0 {
|
||||
requestBody, ok := rb.requestMap["body"].(map[string]interface{})
|
||||
if ok {
|
||||
body, err := json.Marshal(requestBody)
|
||||
if err == nil {
|
||||
rb.req.Body = io.NopCloser(bytes.NewReader(body))
|
||||
rb.req.ContentLength = int64(len(body))
|
||||
}
|
||||
}
|
||||
headers, ok := rb.requestMap["headers"].(map[string]string)
|
||||
rb.req.Header = map[string][]string{}
|
||||
for key, value := range headers {
|
||||
rb.req.Header.Set(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
// log & print request
|
||||
@@ -414,13 +438,19 @@ func runStepRequest(r *SessionRunner, step *TStep) (stepResult *StepResult, err
|
||||
|
||||
// add response object to step variables, could be used in teardown hooks
|
||||
stepVariables["hrp_step_response"] = respObj.respObjMeta
|
||||
stepVariables["response"] = respObj.respObjMeta
|
||||
|
||||
// deal with teardown hooks
|
||||
for _, teardownHook := range step.TeardownHooks {
|
||||
_, err = parser.Parse(teardownHook, stepVariables)
|
||||
res, err := parser.Parse(teardownHook, stepVariables)
|
||||
if err != nil {
|
||||
return stepResult, errors.Wrap(err, "run teardown hooks failed")
|
||||
}
|
||||
resMpa, ok := res.(map[string]interface{})
|
||||
if ok {
|
||||
stepVariables["response"] = resMpa
|
||||
respObj.respObjMeta = resMpa
|
||||
}
|
||||
}
|
||||
|
||||
sessionData.ReqResps.Request = rb.requestMap
|
||||
|
||||
@@ -153,7 +153,7 @@ func TestRunRequestStatOn(t *testing.T) {
|
||||
if !assert.Greater(t, stat["Total"], int64(1)) {
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Less(t, stat["Total"]-summary.Records[0].Elapsed, int64(3)) {
|
||||
if !assert.Less(t, stat["Total"]-summary.Records[0].Elapsed, int64(100)) {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
@@ -164,8 +164,8 @@ func TestRunCaseWithTimeout(t *testing.T) {
|
||||
// global timeout
|
||||
testcase1 := &TestCase{
|
||||
Config: NewConfig("TestCase1").
|
||||
SetTimeout(2 * time.Second). // set global timeout to 2s
|
||||
SetBaseURL("http://httpbin.org"),
|
||||
SetTimeout(10 * time.Second). // set global timeout to 10s
|
||||
SetBaseURL("https://httpbin.org"),
|
||||
TestSteps: []IStep{
|
||||
NewStep("step1").
|
||||
GET("/delay/1").
|
||||
@@ -180,11 +180,11 @@ func TestRunCaseWithTimeout(t *testing.T) {
|
||||
|
||||
testcase2 := &TestCase{
|
||||
Config: NewConfig("TestCase2").
|
||||
SetTimeout(2 * time.Second). // set global timeout to 2s
|
||||
SetBaseURL("http://httpbin.org"),
|
||||
SetTimeout(10 * time.Second). // set global timeout to 10s
|
||||
SetBaseURL("https://httpbin.org"),
|
||||
TestSteps: []IStep{
|
||||
NewStep("step1").
|
||||
GET("/delay/3").
|
||||
GET("/delay/11").
|
||||
Validate().
|
||||
AssertEqual("status_code", 200, "check status code"),
|
||||
},
|
||||
@@ -197,12 +197,12 @@ func TestRunCaseWithTimeout(t *testing.T) {
|
||||
// step timeout
|
||||
testcase3 := &TestCase{
|
||||
Config: NewConfig("TestCase3").
|
||||
SetTimeout(2 * time.Second).
|
||||
SetBaseURL("http://httpbin.org"),
|
||||
SetTimeout(10 * time.Second).
|
||||
SetBaseURL("https://httpbin.org"),
|
||||
TestSteps: []IStep{
|
||||
NewStep("step2").
|
||||
GET("/delay/3").
|
||||
SetTimeout(4*time.Second). // set step timeout to 4s
|
||||
GET("/delay/11").
|
||||
SetTimeout(15*time.Second). // set step timeout to 4s
|
||||
Validate().
|
||||
AssertEqual("status_code", 200, "check status code"),
|
||||
},
|
||||
|
||||
@@ -89,6 +89,8 @@ func (s *StepTestCaseWithOptionalArgs) Run(r *SessionRunner) (stepResult *StepRe
|
||||
return stepResult, err
|
||||
}
|
||||
sessionRunner := caseRunner.NewSession()
|
||||
// need to inherit some information from current session
|
||||
sessionRunner.inheritConnection(r)
|
||||
|
||||
start := time.Now()
|
||||
// run referenced testcase with step variables
|
||||
|
||||
@@ -314,7 +314,7 @@ func runStepWebSocket(r *SessionRunner, step *TStep) (stepResult *StepResult, er
|
||||
case wsOpen:
|
||||
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 {
|
||||
if r.getWsClient(parsedURL) != nil {
|
||||
break
|
||||
}
|
||||
resp, err = openWithTimeout(parsedURL, parsedHeader, r, step)
|
||||
@@ -476,10 +476,15 @@ func openWithTimeout(urlStr string, requestHeader http.Header, r *SessionRunner,
|
||||
conn.SetCloseHandler(func(code int, text string) error {
|
||||
message := websocket.FormatCloseMessage(code, "")
|
||||
conn.WriteControl(websocket.CloseMessage, message, time.Now().Add(defaultWriteWait))
|
||||
r.closeResponseChan <- &wsCloseRespObject{
|
||||
select {
|
||||
case r.closeResponseChan <- &wsCloseRespObject{
|
||||
StatusCode: code,
|
||||
Text: text,
|
||||
}:
|
||||
default:
|
||||
log.Warn().Msg("close response channel is block, drop the response")
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
r.wsConnMap[urlStr] = conn
|
||||
@@ -499,7 +504,7 @@ func openWithTimeout(urlStr string, requestHeader http.Header, r *SessionRunner,
|
||||
}
|
||||
|
||||
func readMessageWithTimeout(urlString string, r *SessionRunner, step *TStep) (*wsReadRespObject, error) {
|
||||
wsConn := r.wsConnMap[urlString]
|
||||
wsConn := r.getWsClient(urlString)
|
||||
if wsConn == nil {
|
||||
return nil, errors.New("try to use existing connection, but there is no connection")
|
||||
}
|
||||
@@ -529,7 +534,7 @@ func readMessageWithTimeout(urlString string, r *SessionRunner, step *TStep) (*w
|
||||
}
|
||||
|
||||
func writeWebSocket(urlString string, r *SessionRunner, step *TStep, stepVariables map[string]interface{}) error {
|
||||
wsConn := r.wsConnMap[urlString]
|
||||
wsConn := r.getWsClient(urlString)
|
||||
if wsConn == nil {
|
||||
return errors.New("try to use existing connection, but there is no connection")
|
||||
}
|
||||
@@ -595,7 +600,7 @@ func writeWithAction(c *websocket.Conn, step *TStep, messageType int, message []
|
||||
}
|
||||
|
||||
func closeWithTimeout(urlString string, r *SessionRunner, step *TStep, stepVariables map[string]interface{}) (*wsCloseRespObject, error) {
|
||||
wsConn := r.wsConnMap[urlString]
|
||||
wsConn := r.getWsClient(urlString)
|
||||
if wsConn == nil {
|
||||
return nil, errors.New("no connection needs to be closed")
|
||||
}
|
||||
|
||||
@@ -8,10 +8,10 @@ class TestHttpSession(unittest.TestCase):
|
||||
self.session = HttpSession()
|
||||
|
||||
def test_request_http(self):
|
||||
self.session.request("get", "http://httpbin.org/get")
|
||||
self.session.request("get", "https://httpbin.org/get")
|
||||
address = self.session.data.address
|
||||
self.assertGreater(len(address.server_ip), 0)
|
||||
self.assertEqual(address.server_port, 80)
|
||||
self.assertEqual(address.server_port, 443)
|
||||
self.assertGreater(len(address.client_ip), 0)
|
||||
self.assertGreater(address.client_port, 10000)
|
||||
|
||||
@@ -26,7 +26,7 @@ class TestHttpSession(unittest.TestCase):
|
||||
def test_request_http_allow_redirects(self):
|
||||
self.session.request(
|
||||
"get",
|
||||
"http://httpbin.org/redirect-to?url=https%3A%2F%2Fgithub.com",
|
||||
"https://httpbin.org/redirect-to?url=https%3A%2F%2Fgithub.com",
|
||||
allow_redirects=True,
|
||||
)
|
||||
address = self.session.data.address
|
||||
@@ -50,7 +50,7 @@ class TestHttpSession(unittest.TestCase):
|
||||
def test_request_http_not_allow_redirects(self):
|
||||
self.session.request(
|
||||
"get",
|
||||
"http://httpbin.org/redirect-to?url=https%3A%2F%2Fgithub.com",
|
||||
"https://httpbin.org/redirect-to?url=https%3A%2F%2Fgithub.com",
|
||||
allow_redirects=False,
|
||||
)
|
||||
address = self.session.data.address
|
||||
|
||||
@@ -10,7 +10,7 @@ Then you can write upload test script as below:
|
||||
- test:
|
||||
name: upload file
|
||||
request:
|
||||
url: http://httpbin.org/upload
|
||||
url: https://httpbin.org/upload
|
||||
method: POST
|
||||
headers:
|
||||
Cookie: session=AAA-BBB-CCC
|
||||
@@ -31,7 +31,7 @@ For compatibility, you can also write upload test script in old way:
|
||||
field2: "value2"
|
||||
m_encoder: ${multipart_encoder(file=$file, field1=$field1, field2=$field2)}
|
||||
request:
|
||||
url: http://httpbin.org/upload
|
||||
url: https://httpbin.org/upload
|
||||
method: POST
|
||||
headers:
|
||||
Content-Type: ${multipart_content_type($m_encoder)}
|
||||
@@ -75,7 +75,9 @@ def ensure_upload_ready():
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def prepare_upload_step(step: TStep, step_variables: VariablesMapping, functions: FunctionsMapping):
|
||||
def prepare_upload_step(
|
||||
step: TStep, step_variables: VariablesMapping, functions: FunctionsMapping
|
||||
):
|
||||
"""preprocess for upload test
|
||||
replace `upload` info with MultipartEncoder
|
||||
|
||||
@@ -84,7 +86,7 @@ def prepare_upload_step(step: TStep, step_variables: VariablesMapping, functions
|
||||
{
|
||||
"variables": {},
|
||||
"request": {
|
||||
"url": "http://httpbin.org/upload",
|
||||
"url": "https://httpbin.org/upload",
|
||||
"method": "POST",
|
||||
"headers": {
|
||||
"Cookie": "session=AAA-BBB-CCC"
|
||||
|
||||
115
poetry.lock
generated
115
poetry.lock
generated
@@ -56,10 +56,10 @@ optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
|
||||
[package.extras]
|
||||
dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"]
|
||||
docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"]
|
||||
tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"]
|
||||
tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"]
|
||||
dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "six", "sphinx", "sphinx-notfound-page", "zope.interface"]
|
||||
docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"]
|
||||
tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "six", "zope.interface"]
|
||||
tests-no-zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "six"]
|
||||
|
||||
[package.source]
|
||||
type = "legacy"
|
||||
@@ -109,11 +109,11 @@ reference = "tsinghua"
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2021.10.8"
|
||||
version = "2022.12.7"
|
||||
description = "Python package for providing Mozilla's CA Bundle."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.source]
|
||||
type = "legacy"
|
||||
@@ -129,7 +129,7 @@ optional = false
|
||||
python-versions = ">=3.5.0"
|
||||
|
||||
[package.extras]
|
||||
unicode_backport = ["unicodedata2"]
|
||||
unicode-backport = ["unicodedata2"]
|
||||
|
||||
[package.source]
|
||||
type = "legacy"
|
||||
@@ -214,7 +214,7 @@ optional = true
|
||||
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*"
|
||||
|
||||
[package.extras]
|
||||
docs = ["sphinx"]
|
||||
docs = ["Sphinx"]
|
||||
|
||||
[package.source]
|
||||
type = "legacy"
|
||||
@@ -247,9 +247,9 @@ typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""}
|
||||
zipp = ">=0.5"
|
||||
|
||||
[package.extras]
|
||||
docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)"]
|
||||
docs = ["jaraco.packaging (>=9)", "rst.linker (>=1.9)", "sphinx"]
|
||||
perf = ["ipython"]
|
||||
testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)", "importlib-resources (>=1.3)"]
|
||||
testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"]
|
||||
|
||||
[package.source]
|
||||
type = "legacy"
|
||||
@@ -314,7 +314,7 @@ colorama = {version = ">=0.3.4", markers = "sys_platform == \"win32\""}
|
||||
win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""}
|
||||
|
||||
[package.extras]
|
||||
dev = ["codecov (>=2.0.15)", "colorama (>=0.3.4)", "flake8 (>=3.7.7)", "isort (>=4.3.20)", "tox (>=3.9.0)", "tox-travis (>=0.12)", "pytest (>=4.6.2)", "pytest-cov (>=2.7.1)", "Sphinx (>=2.2.1)", "sphinx-autobuild (>=0.7.1)", "sphinx-rtd-theme (>=0.4.3)", "black (>=19.3b0)"]
|
||||
dev = ["Sphinx (>=2.2.1)", "black (>=19.3b0)", "codecov (>=2.0.15)", "colorama (>=0.3.4)", "flake8 (>=3.7.7)", "isort (>=4.3.20)", "pytest (>=4.6.2)", "pytest-cov (>=2.7.1)", "sphinx-autobuild (>=0.7.1)", "sphinx-rtd-theme (>=0.4.3)", "tox (>=3.9.0)", "tox-travis (>=0.12)"]
|
||||
|
||||
[package.source]
|
||||
type = "legacy"
|
||||
@@ -385,8 +385,8 @@ optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[package.extras]
|
||||
docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)", "sphinx (>=4)"]
|
||||
test = ["appdirs (==1.4.4)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)", "pytest (>=6)"]
|
||||
docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx (>=4)", "sphinx-autodoc-typehints (>=1.12)"]
|
||||
test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"]
|
||||
|
||||
[package.source]
|
||||
type = "legacy"
|
||||
@@ -485,7 +485,7 @@ optional = false
|
||||
python-versions = ">=3.6.8"
|
||||
|
||||
[package.extras]
|
||||
diagrams = ["railroad-diagrams", "jinja2"]
|
||||
diagrams = ["jinja2", "railroad-diagrams"]
|
||||
|
||||
[package.source]
|
||||
type = "legacy"
|
||||
@@ -581,7 +581,7 @@ urllib3 = ">=1.21.1,<1.27"
|
||||
|
||||
[package.extras]
|
||||
socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"]
|
||||
use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"]
|
||||
use-chardet-on-py3 = ["chardet (>=3.0.2,<5)"]
|
||||
|
||||
[package.source]
|
||||
type = "legacy"
|
||||
@@ -623,7 +623,7 @@ bottle = ["bottle (>=0.12.13)"]
|
||||
celery = ["celery (>=3)"]
|
||||
django = ["django (>=1.8)"]
|
||||
falcon = ["falcon (>=1.4)"]
|
||||
flask = ["flask (>=0.11)", "blinker (>=1.1)"]
|
||||
flask = ["blinker (>=1.1)", "flask (>=0.11)"]
|
||||
pyspark = ["pyspark (>=2.4.4)"]
|
||||
rq = ["rq (>=0.6)"]
|
||||
sanic = ["sanic (>=0.8)"]
|
||||
@@ -661,25 +661,25 @@ greenlet = {version = "!=0.4.17", markers = "python_version >= \"3\" and (platfo
|
||||
importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
|
||||
|
||||
[package.extras]
|
||||
aiomysql = ["greenlet (!=0.4.17)", "aiomysql"]
|
||||
aiosqlite = ["typing_extensions (!=3.10.0.1)", "greenlet (!=0.4.17)", "aiosqlite"]
|
||||
aiomysql = ["aiomysql", "greenlet (!=0.4.17)"]
|
||||
aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing_extensions (!=3.10.0.1)"]
|
||||
asyncio = ["greenlet (!=0.4.17)"]
|
||||
asyncmy = ["greenlet (!=0.4.17)", "asyncmy (>=0.2.3,!=0.2.4)"]
|
||||
mariadb_connector = ["mariadb (>=1.0.1)"]
|
||||
asyncmy = ["asyncmy (>=0.2.3,!=0.2.4)", "greenlet (!=0.4.17)"]
|
||||
mariadb-connector = ["mariadb (>=1.0.1)"]
|
||||
mssql = ["pyodbc"]
|
||||
mssql_pymssql = ["pymssql"]
|
||||
mssql_pyodbc = ["pyodbc"]
|
||||
mypy = ["sqlalchemy2-stubs", "mypy (>=0.910)"]
|
||||
mysql = ["mysqlclient (>=1.4.0,<2)", "mysqlclient (>=1.4.0)"]
|
||||
mysql_connector = ["mysql-connector-python"]
|
||||
oracle = ["cx_oracle (>=7,<8)", "cx_oracle (>=7)"]
|
||||
mssql-pymssql = ["pymssql"]
|
||||
mssql-pyodbc = ["pyodbc"]
|
||||
mypy = ["mypy (>=0.910)", "sqlalchemy2-stubs"]
|
||||
mysql = ["mysqlclient (>=1.4.0)", "mysqlclient (>=1.4.0,<2)"]
|
||||
mysql-connector = ["mysql-connector-python"]
|
||||
oracle = ["cx_oracle (>=7)", "cx_oracle (>=7,<8)"]
|
||||
postgresql = ["psycopg2 (>=2.7)"]
|
||||
postgresql_asyncpg = ["greenlet (!=0.4.17)", "asyncpg"]
|
||||
postgresql_pg8000 = ["pg8000 (>=1.16.6)"]
|
||||
postgresql_psycopg2binary = ["psycopg2-binary"]
|
||||
postgresql_psycopg2cffi = ["psycopg2cffi"]
|
||||
pymysql = ["pymysql (<1)", "pymysql"]
|
||||
sqlcipher = ["sqlcipher3-binary"]
|
||||
postgresql-asyncpg = ["asyncpg", "greenlet (!=0.4.17)"]
|
||||
postgresql-pg8000 = ["pg8000 (>=1.16.6)"]
|
||||
postgresql-psycopg2binary = ["psycopg2-binary"]
|
||||
postgresql-psycopg2cffi = ["psycopg2cffi"]
|
||||
pymysql = ["pymysql", "pymysql (<1)"]
|
||||
sqlcipher = ["sqlcipher3_binary"]
|
||||
|
||||
[package.source]
|
||||
type = "legacy"
|
||||
@@ -719,7 +719,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
ply = ">=3.4,<4.0"
|
||||
|
||||
[package.extras]
|
||||
dev = ["cython (>=0.28.4)", "flake8 (>=2.5)", "pytest (>=2.8)", "sphinx-rtd-theme (>=0.1.9)", "sphinx (>=1.3)", "tornado (>=4.0,<6.0)"]
|
||||
dev = ["cython (>=0.28.4)", "flake8 (>=2.5)", "pytest (>=2.8)", "sphinx (>=1.3)", "sphinx-rtd-theme (>=0.1.9)", "tornado (>=4.0,<6.0)"]
|
||||
tornado = ["tornado (>=4.0,<6.0)"]
|
||||
|
||||
[package.source]
|
||||
@@ -788,8 +788,8 @@ optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4"
|
||||
|
||||
[package.extras]
|
||||
brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"]
|
||||
secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"]
|
||||
brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"]
|
||||
secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)"]
|
||||
socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
|
||||
|
||||
[package.source]
|
||||
@@ -806,7 +806,7 @@ optional = false
|
||||
python-versions = ">=3.5"
|
||||
|
||||
[package.extras]
|
||||
dev = ["pytest (>=4.6.2)", "black (>=19.3b0)"]
|
||||
dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"]
|
||||
|
||||
[package.source]
|
||||
type = "legacy"
|
||||
@@ -822,8 +822,8 @@ optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[package.extras]
|
||||
docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)"]
|
||||
testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"]
|
||||
docs = ["jaraco.packaging (>=9)", "rst.linker (>=1.9)", "sphinx"]
|
||||
testing = ["func-timeout", "jaraco.itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-flake8", "pytest-mypy (>=0.9.1)"]
|
||||
|
||||
[package.source]
|
||||
type = "legacy"
|
||||
@@ -895,8 +895,21 @@ brotli = [
|
||||
{file = "Brotli-1.0.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ee83d3e3a024a9618e5be64648d6d11c37047ac48adff25f12fa4226cf23d1c"},
|
||||
{file = "Brotli-1.0.9-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:19598ecddd8a212aedb1ffa15763dd52a388518c4550e615aed88dc3753c0f0c"},
|
||||
{file = "Brotli-1.0.9-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:44bb8ff420c1d19d91d79d8c3574b8954288bdff0273bf788954064d260d7ab0"},
|
||||
{file = "Brotli-1.0.9-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e23281b9a08ec338469268f98f194658abfb13658ee98e2b7f85ee9dd06caa91"},
|
||||
{file = "Brotli-1.0.9-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:3496fc835370da351d37cada4cf744039616a6db7d13c430035e901443a34daa"},
|
||||
{file = "Brotli-1.0.9-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b83bb06a0192cccf1eb8d0a28672a1b79c74c3a8a5f2619625aeb6f28b3a82bb"},
|
||||
{file = "Brotli-1.0.9-cp310-cp310-win32.whl", hash = "sha256:26d168aac4aaec9a4394221240e8a5436b5634adc3cd1cdf637f6645cecbf181"},
|
||||
{file = "Brotli-1.0.9-cp310-cp310-win_amd64.whl", hash = "sha256:622a231b08899c864eb87e85f81c75e7b9ce05b001e59bbfbf43d4a71f5f32b2"},
|
||||
{file = "Brotli-1.0.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:cc0283a406774f465fb45ec7efb66857c09ffefbe49ec20b7882eff6d3c86d3a"},
|
||||
{file = "Brotli-1.0.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:11d3283d89af7033236fa4e73ec2cbe743d4f6a81d41bd234f24bf63dde979df"},
|
||||
{file = "Brotli-1.0.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c1306004d49b84bd0c4f90457c6f57ad109f5cc6067a9664e12b7b79a9948ad"},
|
||||
{file = "Brotli-1.0.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1375b5d17d6145c798661b67e4ae9d5496920d9265e2f00f1c2c0b5ae91fbde"},
|
||||
{file = "Brotli-1.0.9-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cab1b5964b39607a66adbba01f1c12df2e55ac36c81ec6ed44f2fca44178bf1a"},
|
||||
{file = "Brotli-1.0.9-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:8ed6a5b3d23ecc00ea02e1ed8e0ff9a08f4fc87a1f58a2530e71c0f48adf882f"},
|
||||
{file = "Brotli-1.0.9-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cb02ed34557afde2d2da68194d12f5719ee96cfb2eacc886352cb73e3808fc5d"},
|
||||
{file = "Brotli-1.0.9-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b3523f51818e8f16599613edddb1ff924eeb4b53ab7e7197f85cbc321cdca32f"},
|
||||
{file = "Brotli-1.0.9-cp311-cp311-win32.whl", hash = "sha256:ba72d37e2a924717990f4d7482e8ac88e2ef43fb95491eb6e0d124d77d2a150d"},
|
||||
{file = "Brotli-1.0.9-cp311-cp311-win_amd64.whl", hash = "sha256:3ffaadcaeafe9d30a7e4e1e97ad727e4f5610b9fa2f7551998471e3736738679"},
|
||||
{file = "Brotli-1.0.9-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:c83aa123d56f2e060644427a882a36b3c12db93727ad7a7b9efd7d7f3e9cc2c4"},
|
||||
{file = "Brotli-1.0.9-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:6b2ae9f5f67f89aade1fab0f7fd8f2832501311c363a21579d02defa844d9296"},
|
||||
{file = "Brotli-1.0.9-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:68715970f16b6e92c574c30747c95cf8cf62804569647386ff032195dc89a430"},
|
||||
@@ -906,12 +919,18 @@ brotli = [
|
||||
{file = "Brotli-1.0.9-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:40d15c79f42e0a2c72892bf407979febd9cf91f36f495ffb333d1d04cebb34e4"},
|
||||
{file = "Brotli-1.0.9-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:93130612b837103e15ac3f9cbacb4613f9e348b58b3aad53721d92e57f96d46a"},
|
||||
{file = "Brotli-1.0.9-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87fdccbb6bb589095f413b1e05734ba492c962b4a45a13ff3408fa44ffe6479b"},
|
||||
{file = "Brotli-1.0.9-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:6d847b14f7ea89f6ad3c9e3901d1bc4835f6b390a9c71df999b0162d9bb1e20f"},
|
||||
{file = "Brotli-1.0.9-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:495ba7e49c2db22b046a53b469bbecea802efce200dffb69b93dd47397edc9b6"},
|
||||
{file = "Brotli-1.0.9-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:4688c1e42968ba52e57d8670ad2306fe92e0169c6f3af0089be75bbac0c64a3b"},
|
||||
{file = "Brotli-1.0.9-cp36-cp36m-win32.whl", hash = "sha256:61a7ee1f13ab913897dac7da44a73c6d44d48a4adff42a5701e3239791c96e14"},
|
||||
{file = "Brotli-1.0.9-cp36-cp36m-win_amd64.whl", hash = "sha256:1c48472a6ba3b113452355b9af0a60da5c2ae60477f8feda8346f8fd48e3e87c"},
|
||||
{file = "Brotli-1.0.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3b78a24b5fd13c03ee2b7b86290ed20efdc95da75a3557cc06811764d5ad1126"},
|
||||
{file = "Brotli-1.0.9-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:9d12cf2851759b8de8ca5fde36a59c08210a97ffca0eb94c532ce7b17c6a3d1d"},
|
||||
{file = "Brotli-1.0.9-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:6c772d6c0a79ac0f414a9f8947cc407e119b8598de7621f39cacadae3cf57d12"},
|
||||
{file = "Brotli-1.0.9-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29d1d350178e5225397e28ea1b7aca3648fcbab546d20e7475805437bfb0a130"},
|
||||
{file = "Brotli-1.0.9-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7bbff90b63328013e1e8cb50650ae0b9bac54ffb4be6104378490193cd60f85a"},
|
||||
{file = "Brotli-1.0.9-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:ec1947eabbaf8e0531e8e899fc1d9876c179fc518989461f5d24e2223395a9e3"},
|
||||
{file = "Brotli-1.0.9-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:12effe280b8ebfd389022aa65114e30407540ccb89b177d3fbc9a4f177c4bd5d"},
|
||||
{file = "Brotli-1.0.9-cp37-cp37m-win32.whl", hash = "sha256:f909bbbc433048b499cb9db9e713b5d8d949e8c109a2a548502fb9aa8630f0b1"},
|
||||
{file = "Brotli-1.0.9-cp37-cp37m-win_amd64.whl", hash = "sha256:97f715cf371b16ac88b8c19da00029804e20e25f30d80203417255d239f228b5"},
|
||||
{file = "Brotli-1.0.9-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e16eb9541f3dd1a3e92b89005e37b1257b157b7256df0e36bd7b33b50be73bcb"},
|
||||
@@ -919,6 +938,9 @@ brotli = [
|
||||
{file = "Brotli-1.0.9-cp38-cp38-manylinux1_i686.whl", hash = "sha256:b663f1e02de5d0573610756398e44c130add0eb9a3fc912a09665332942a2efb"},
|
||||
{file = "Brotli-1.0.9-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:5b6ef7d9f9c38292df3690fe3e302b5b530999fa90014853dcd0d6902fb59f26"},
|
||||
{file = "Brotli-1.0.9-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a674ac10e0a87b683f4fa2b6fa41090edfd686a6524bd8dedbd6138b309175c"},
|
||||
{file = "Brotli-1.0.9-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e2d9e1cbc1b25e22000328702b014227737756f4b5bf5c485ac1d8091ada078b"},
|
||||
{file = "Brotli-1.0.9-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:b336c5e9cf03c7be40c47b5fd694c43c9f1358a80ba384a21969e0b4e66a9b17"},
|
||||
{file = "Brotli-1.0.9-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:85f7912459c67eaab2fb854ed2bc1cc25772b300545fe7ed2dc03954da638649"},
|
||||
{file = "Brotli-1.0.9-cp38-cp38-win32.whl", hash = "sha256:35a3edbe18e876e596553c4007a087f8bcfd538f19bc116917b3c7522fca0429"},
|
||||
{file = "Brotli-1.0.9-cp38-cp38-win_amd64.whl", hash = "sha256:269a5743a393c65db46a7bb982644c67ecba4b8d91b392403ad8a861ba6f495f"},
|
||||
{file = "Brotli-1.0.9-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2aad0e0baa04517741c9bb5b07586c642302e5fb3e75319cb62087bd0995ab19"},
|
||||
@@ -926,15 +948,28 @@ brotli = [
|
||||
{file = "Brotli-1.0.9-cp39-cp39-manylinux1_i686.whl", hash = "sha256:16d528a45c2e1909c2798f27f7bf0a3feec1dc9e50948e738b961618e38b6a7b"},
|
||||
{file = "Brotli-1.0.9-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:56d027eace784738457437df7331965473f2c0da2c70e1a1f6fdbae5402e0389"},
|
||||
{file = "Brotli-1.0.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9bf919756d25e4114ace16a8ce91eb340eb57a08e2c6950c3cebcbe3dff2a5e7"},
|
||||
{file = "Brotli-1.0.9-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e4c4e92c14a57c9bd4cb4be678c25369bf7a092d55fd0866f759e425b9660806"},
|
||||
{file = "Brotli-1.0.9-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:e48f4234f2469ed012a98f4b7874e7f7e173c167bed4934912a29e03167cf6b1"},
|
||||
{file = "Brotli-1.0.9-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9ed4c92a0665002ff8ea852353aeb60d9141eb04109e88928026d3c8a9e5433c"},
|
||||
{file = "Brotli-1.0.9-cp39-cp39-win32.whl", hash = "sha256:cfc391f4429ee0a9370aa93d812a52e1fee0f37a81861f4fdd1f4fb28e8547c3"},
|
||||
{file = "Brotli-1.0.9-cp39-cp39-win_amd64.whl", hash = "sha256:854c33dad5ba0fbd6ab69185fec8dab89e13cda6b7d191ba111987df74f38761"},
|
||||
{file = "Brotli-1.0.9-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9749a124280a0ada4187a6cfd1ffd35c350fb3af79c706589d98e088c5044267"},
|
||||
{file = "Brotli-1.0.9-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:73fd30d4ce0ea48010564ccee1a26bfe39323fde05cb34b5863455629db61dc7"},
|
||||
{file = "Brotli-1.0.9-pp37-pypy37_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:02177603aaca36e1fd21b091cb742bb3b305a569e2402f1ca38af471777fb019"},
|
||||
{file = "Brotli-1.0.9-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:76ffebb907bec09ff511bb3acc077695e2c32bc2142819491579a695f77ffd4d"},
|
||||
{file = "Brotli-1.0.9-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b43775532a5904bc938f9c15b77c613cb6ad6fb30990f3b0afaea82797a402d8"},
|
||||
{file = "Brotli-1.0.9-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5bf37a08493232fbb0f8229f1824b366c2fc1d02d64e7e918af40acd15f3e337"},
|
||||
{file = "Brotli-1.0.9-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:330e3f10cd01da535c70d09c4283ba2df5fb78e915bea0a28becad6e2ac010be"},
|
||||
{file = "Brotli-1.0.9-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:e1abbeef02962596548382e393f56e4c94acd286bd0c5afba756cffc33670e8a"},
|
||||
{file = "Brotli-1.0.9-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3148362937217b7072cf80a2dcc007f09bb5ecb96dae4617316638194113d5be"},
|
||||
{file = "Brotli-1.0.9-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:336b40348269f9b91268378de5ff44dc6fbaa2268194f85177b53463d313842a"},
|
||||
{file = "Brotli-1.0.9-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3b8b09a16a1950b9ef495a0f8b9d0a87599a9d1f179e2d4ac014b2ec831f87e7"},
|
||||
{file = "Brotli-1.0.9-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:c8e521a0ce7cf690ca84b8cc2272ddaf9d8a50294fd086da67e517439614c755"},
|
||||
{file = "Brotli-1.0.9.zip", hash = "sha256:4d1b810aa0ed773f81dceda2cc7b403d01057458730e309856356d4ef4188438"},
|
||||
]
|
||||
certifi = [
|
||||
{file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"},
|
||||
{file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"},
|
||||
{file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"},
|
||||
{file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"},
|
||||
]
|
||||
charset-normalizer = [
|
||||
{file = "charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"},
|
||||
|
||||
Reference in New Issue
Block a user