Merge pull request #1671 from httprunner/video-crawler-plugin

**go version**

plugin related:

- feat: add hrp executable directory for searching plugin
- feat: init device driver with plugin option
- feat: support printing stderr output in myexec.RunCommand
- change: upgrade funplugin to 0.5.3
- refactor: move internal myexec to funplugin/myexec
- change: create python3 plugin venv with latest funppy/httprunner

others:

- change: log elapsed duration in miliseconds
This commit is contained in:
debugtalk
2023-08-25 11:25:33 +08:00
committed by GitHub
57 changed files with 848 additions and 929 deletions

View File

@@ -1,15 +1,34 @@
# Release History
## v4.3.6-beta (2023-08-11)
## v4.3.6 (2023-08-24)
**go version**
plugin related:
- feat: add hrp executable directory for searching plugin
- feat: init device driver with plugin options, `WithDriverCapabilities` and `WithDriverPlugin`
- feat: support printing stderr output in myexec.RunCommand
- change: upgrade funplugin to 0.5.3
- refactor: move internal myexec to funplugin/myexec
- change: create python3 plugin venv with latest funppy/httprunner
UI related:
- feat: get current feed info from app event trackings
- feat: log feed screenshot take/cv elapsed time
- feat: support to reset driver (or session only) automatically when UIA2 / WDA crashed or WebDriver request failed
- feat: `tap_cv` action supports ui type detection and tap
- compatibility: support indicating options separately in `MobileAction` level
- feat: support action options for `ScreenShot`, `WithScreenShotOCR`/`WithScreenShotUpload`/`WithScreenShotLiveType`/`WithScreenShotUIType`
- fix: add compatible support for indicating options separately at the `MobileAction` level
- fix: use Override size if existed, otherwise use Physical size (android devices)
- fix: add default options for `swipe_to_tap_app` action
- refactor: ui validation methods
- fix: reuse the same request body during `GetImage` retry
- change: upgrade funplugin to 0.5.2
others:
- change: log elapsed duration in milliseconds
- change: set log timestamp precise to milliseconds
## v4.3.5 (2023-07-23)

View File

@@ -37,4 +37,4 @@ Copyright 2017 debugtalk
* [hrp startproject](hrp_startproject.md) - create a scaffold project
* [hrp wiki](hrp_wiki.md) - visit https://httprunner.com
###### Auto generated by spf13/cobra on 31-May-2023
###### Auto generated by spf13/cobra on 20-Aug-2023

View File

@@ -54,4 +54,4 @@ hrp boom [flags]
* [hrp](hrp.md) - Next-Generation API Testing Solution.
###### Auto generated by spf13/cobra on 31-May-2023
###### Auto generated by spf13/cobra on 20-Aug-2023

View File

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

View File

@@ -26,4 +26,4 @@ hrp convert $path... [flags]
* [hrp](hrp.md) - Next-Generation API Testing Solution.
###### Auto generated by spf13/cobra on 31-May-2023
###### Auto generated by spf13/cobra on 20-Aug-2023

View File

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

View File

@@ -36,4 +36,4 @@ hrp run $path... [flags]
* [hrp](hrp.md) - Next-Generation API Testing Solution.
###### Auto generated by spf13/cobra on 31-May-2023
###### Auto generated by spf13/cobra on 20-Aug-2023

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
{
"project_name": "demo-empty-project",
"create_time": "2023-07-23T13:54:23.516072+08:00",
"hrp_version": "v4.3.5"
"create_time": "2023-08-24T21:20:48.397396+08:00",
"hrp_version": "v4.3.6"
}

View File

@@ -1,5 +1,5 @@
{
"project_name": "demo-with-go-plugin",
"create_time": "2023-07-23T14:30:10.985053+08:00",
"hrp_version": "v4.3.5"
"create_time": "2023-08-24T21:20:18.184017+08:00",
"hrp_version": "v4.3.6"
}

View File

@@ -1,5 +1,5 @@
{
"project_name": "demo-with-py-plugin",
"create_time": "2023-07-23T14:30:18.556239+08:00",
"hrp_version": "v4.3.5"
"create_time": "2023-08-24T21:20:23.45156+08:00",
"hrp_version": "v4.3.6"
}

View File

@@ -1,5 +1,5 @@
{
"project_name": "demo-without-plugin",
"create_time": "2023-07-23T13:54:23.368356+08:00",
"hrp_version": "v4.3.5"
"create_time": "2023-08-24T21:20:47.861732+08:00",
"hrp_version": "v4.3.6"
}

View File

@@ -11,6 +11,29 @@
]
},
"teststeps": [
{
"name": "启动 app",
"android": {
"actions": [
{
"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": "滑动消费 feed 至少 10 个live 至少 3 个滑动过程中70% 随机间隔 0-5s30% 随机间隔 5-10s",
"android": {
@@ -18,7 +41,6 @@
{
"method": "video_crawler",
"params": {
"app_package_name": "com.ss.android.ugc.aweme",
"feed": {
"sleep_random": [
0,

View File

@@ -17,11 +17,17 @@ func TestAndroidVideoCrawlerTest(t *testing.T) {
}).
SetAndroid(uixt.WithSerialNumber("$device")),
TestSteps: []hrp.IStep{
hrp.NewStep("启动 app").
Android().
ScreenShot(uixt.WithScreenShotOCR(true), uixt.WithScreenShotUpload(true)).
AppLaunch("com.ss.android.ugc.aweme").
Sleep(5).
Validate().
AssertAppInForeground("com.ss.android.ugc.aweme"),
hrp.NewStep("滑动消费 feed 至少 10 个live 至少 3 个滑动过程中70% 随机间隔 0-5s30% 随机间隔 5-10s").
Android().
VideoCrawler(map[string]interface{}{
"app_package_name": "com.ss.android.ugc.aweme",
"timeout": 600,
"timeout": 600,
"feed": map[string]interface{}{
"target_count": 5,
"target_labels": []map[string]interface{}{

View File

@@ -102,7 +102,7 @@ type WorldCupLive struct {
}
func NewWorldCupLive(device uixt.Device, matchName, bundleID string, duration, interval int) *WorldCupLive {
driverExt, err := device.NewDriver(nil)
driverExt, err := device.NewDriver()
if err != nil {
log.Fatal().Err(err).Msg("failed to init driver")
}

4
go.mod
View File

@@ -10,7 +10,7 @@ require (
github.com/go-openapi/spec v0.20.7
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
github.com/gorilla/websocket v1.5.0
github.com/httprunner/funplugin v0.5.2
github.com/httprunner/funplugin v0.5.3
github.com/jinzhu/copier v0.3.5
github.com/jmespath/go-jmespath v0.4.0
github.com/json-iterator/go v1.1.12
@@ -76,7 +76,7 @@ require (
golang.org/x/sys v0.11.0 // indirect
golang.org/x/text v0.12.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230807174057-1744710a1577 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230815205213-6bfd019c3878 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)

8
go.sum
View File

@@ -171,8 +171,8 @@ github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE=
github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
github.com/httprunner/funplugin v0.5.2 h1:VgDkHXNEo55KRgNZz+1oW8JboSNpfufNOfPD7l0LGyI=
github.com/httprunner/funplugin v0.5.2/go.mod h1:YZzBBSOSdLZEpHZz0P2E5SOQ+o1+Fbn30oWS4RGHBz0=
github.com/httprunner/funplugin v0.5.3 h1:OHYXqq8fuO/qzT+TzXxhS3HVfKdb8kh+Q/0/S3n4afA=
github.com/httprunner/funplugin v0.5.3/go.mod h1:YZzBBSOSdLZEpHZz0P2E5SOQ+o1+Fbn30oWS4RGHBz0=
github.com/hybridgroup/mjpeg v0.0.0-20140228234708-4680f319790e/go.mod h1:eagM805MRKrioHYuU7iKLUyFPVKqVV6um5DAvCkUtXs=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
@@ -579,8 +579,8 @@ google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7Fc
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230807174057-1744710a1577 h1:wukfNtZmZUurLN/atp2hiIeTKn7QJWIQdHzqmsOnAOk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230807174057-1744710a1577/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230815205213-6bfd019c3878 h1:lv6/DhyiFFGsmzxbsUUTOkN29II+zeWHxvT8Lpdxsv0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230815205213-6bfd019c3878/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=

View File

@@ -11,13 +11,13 @@ import (
"strings"
"github.com/httprunner/funplugin/fungo"
"github.com/httprunner/funplugin/myexec"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
"github.com/httprunner/httprunner/v4/hrp/internal/code"
"github.com/httprunner/httprunner/v4/hrp/internal/env"
"github.com/httprunner/httprunner/v4/hrp/internal/myexec"
"github.com/httprunner/httprunner/v4/hrp/internal/sdk"
"github.com/httprunner/httprunner/v4/hrp/internal/version"
)

View File

@@ -1,16 +1,16 @@
package cmd
import (
"fmt"
"io/ioutil"
"path/filepath"
"github.com/httprunner/funplugin/myexec"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
"github.com/httprunner/httprunner/v4/hrp/internal/myexec"
"github.com/httprunner/httprunner/v4/hrp/internal/version"
"github.com/httprunner/httprunner/v4/hrp/internal/code"
"github.com/httprunner/httprunner/v4/hrp/pkg/convert"
)
@@ -40,13 +40,11 @@ var convertCmd = &cobra.Command{
if toYAMLFlag {
outputType = convert.OutputTypeYAML
} else if toPyTestFlag {
packages := []string{
fmt.Sprintf("httprunner==%s", version.HttpRunnerMinimumVersion),
}
packages := []string{"httprunner"}
_, err := myexec.EnsurePython3Venv(venv, packages...)
if err != nil {
log.Error().Err(err).Msg("python3 venv is not ready")
return err
return errors.Wrap(code.InvalidPython3Venv, err.Error())
}
outputType = convert.OutputTypePyTest

View File

@@ -1,17 +1,17 @@
package cmd
import (
"fmt"
"strings"
"time"
"github.com/httprunner/funplugin/myexec"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
"github.com/httprunner/httprunner/v4/hrp/internal/myexec"
"github.com/httprunner/httprunner/v4/hrp/internal/code"
"github.com/httprunner/httprunner/v4/hrp/internal/pytest"
"github.com/httprunner/httprunner/v4/hrp/internal/sdk"
"github.com/httprunner/httprunner/v4/hrp/internal/version"
)
var pytestCmd = &cobra.Command{
@@ -29,13 +29,11 @@ var pytestCmd = &cobra.Command{
})
}()
packages := []string{
fmt.Sprintf("httprunner==%s", version.HttpRunnerMinimumVersion),
}
packages := []string{"httprunner"}
_, err = myexec.EnsurePython3Venv(venv, packages...)
if err != nil {
log.Error().Err(err).Msg("python3 venv is not ready")
return err
return errors.Wrap(code.InvalidPython3Venv, err.Error())
}
return pytest.RunPytest(args)
},

View File

@@ -1,6 +1,7 @@
package cmd
import (
"io"
"os"
"runtime"
"strings"
@@ -69,6 +70,11 @@ func initLogger(logLevel string, logJSON bool) {
// Error Logging with Stacktrace
zerolog.ErrorStackMarshaler = pkgerrors.MarshalStack
// set log timestamp precise to milliseconds
zerolog.TimeFieldFormat = "2006-01-02T15:04:05.999Z0700"
// init log writer
var writer io.Writer
if !logJSON {
// log a human-friendly, colorized output
noColor := false
@@ -76,19 +82,18 @@ func initLogger(logLevel string, logJSON bool) {
noColor = true
}
log.Logger = zerolog.New(
zerolog.ConsoleWriter{
Out: os.Stderr,
TimeFormat: time.RFC3339,
NoColor: noColor,
},
).With().Timestamp().Logger()
writer = zerolog.ConsoleWriter{
Out: os.Stderr,
TimeFormat: time.RFC3339Nano,
NoColor: noColor,
}
log.Info().Msg("log with colorized console")
} else {
// default logger
log.Info().Msg("log with json output")
log.Logger = zerolog.New(os.Stderr).With().Timestamp().Logger()
writer = os.Stderr
}
log.Logger = zerolog.New(writer).With().Timestamp().Logger()
// Setting Global Log Level
level := strings.ToUpper(logLevel)

View File

@@ -1,163 +0,0 @@
package myexec
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
"github.com/httprunner/httprunner/v4/hrp/internal/code"
"github.com/httprunner/httprunner/v4/hrp/internal/env"
)
var python3Executable string = "python3" // system default python3
func isPython3(python string) bool {
out, err := Command(python, "--version").Output()
if err != nil {
return false
}
if strings.HasPrefix(string(out), "Python 3") {
return true
}
return false
}
// EnsurePython3Venv ensures python3 venv with specified packages
// venv should be directory path of target venv
func EnsurePython3Venv(venv string, packages ...string) (python3 string, err error) {
// priority: specified > $HOME/.hrp/venv
if venv == "" {
home, err := os.UserHomeDir()
if err != nil {
return "", errors.Wrap(err, "get user home dir failed")
}
venv = filepath.Join(home, ".hrp", "venv")
}
python3, err = ensurePython3Venv(venv, packages...)
if err != nil {
return "", errors.Wrap(code.InvalidPython3Venv, err.Error())
}
python3Executable = python3
log.Info().Str("Python3Executable", python3Executable).Msg("set python3 executable path")
return python3, nil
}
func ExecPython3Command(cmdName string, args ...string) error {
args = append([]string{"-m", cmdName}, args...)
return RunCommand(python3Executable, args...)
}
func AssertPythonPackage(python3 string, pkgName, pkgVersion string) error {
out, err := exec.Command(
python3, "-c", fmt.Sprintf("import %s; print(%s.__version__)", pkgName, pkgName),
).Output()
if err != nil {
return fmt.Errorf("python package %s not found", pkgName)
}
// do not check version if pkgVersion is empty
if pkgVersion == "" {
log.Info().Str("name", pkgName).Msg("python package is ready")
return nil
}
// check package version equality
version := strings.TrimSpace(string(out))
if strings.TrimLeft(version, "v") != strings.TrimLeft(pkgVersion, "v") {
return fmt.Errorf("python package %s version %s not matched, please upgrade to %s",
pkgName, version, pkgVersion)
}
log.Info().Str("name", pkgName).Str("version", pkgVersion).Msg("python package is ready")
return nil
}
func InstallPythonPackage(python3 string, pkg string) (err error) {
var pkgName, pkgVersion string
if strings.Contains(pkg, "==") {
// funppy==0.5.0
pkgInfo := strings.Split(pkg, "==")
pkgName = pkgInfo[0]
pkgVersion = pkgInfo[1]
} else {
// funppy
pkgName = pkg
}
// check if package installed and version matched
err = AssertPythonPackage(python3, pkgName, pkgVersion)
if err == nil {
return nil
}
// check if pip available
err = RunCommand(python3, "-m", "pip", "--version")
if err != nil {
log.Warn().Msg("pip is not available")
return errors.Wrap(err, "pip is not available")
}
log.Info().Str("pkgName", pkgName).Str("pkgVersion", pkgVersion).Msg("installing python package")
// install package
pypiIndexURL := env.PYPI_INDEX_URL
if pypiIndexURL == "" {
pypiIndexURL = "https://pypi.org/simple" // default
}
err = RunCommand(python3, "-m", "pip", "install", "--upgrade", pkg,
"--index-url", pypiIndexURL,
"--quiet", "--disable-pip-version-check")
if err != nil {
return errors.Wrap(err, "pip install package failed")
}
return AssertPythonPackage(python3, pkgName, pkgVersion)
}
func RunCommand(cmdName string, args ...string) error {
cmd := Command(cmdName, args...)
log.Info().Str("cmd", cmd.String()).Msg("exec command")
// add cmd dir path to $PATH
if cmdDir := filepath.Dir(cmdName); cmdDir != "" {
path := fmt.Sprintf("%s:%s", cmdDir, env.PATH)
if err := os.Setenv("PATH", path); err != nil {
log.Error().Err(err).Msg("set env $PATH failed")
return err
}
}
// print output with colors
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Run()
if err != nil {
log.Error().Err(err).Msg("exec command failed")
return err
}
return nil
}
func ExecCommandInDir(cmd *exec.Cmd, dir string) error {
log.Info().Str("cmd", cmd.String()).Str("dir", dir).Msg("exec command")
cmd.Dir = dir
// print output with colors
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Run()
if err != nil {
log.Error().Err(err).Msg("exec command failed")
return err
}
return nil
}

View File

@@ -1,71 +0,0 @@
//go:build darwin || linux
package myexec
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"syscall"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
)
func getPython3Executable(venvDir string) string {
return filepath.Join(venvDir, "bin", "python3")
}
func ensurePython3Venv(venv string, packages ...string) (python3 string, err error) {
python3 = getPython3Executable(venv)
log.Info().
Str("python3", python3).
Interface("packages", packages).
Msg("ensure python3 venv")
// check if python3 venv is available
if !isPython3(python3) {
// python3 venv not available, create one
// check if system python3 is available
if err := RunCommand("python3", "--version"); err != nil {
return "", errors.Wrap(err, "python3 not found")
}
// check if .venv exists
if _, err := os.Stat(venv); err == nil {
// .venv exists, remove first
if err := RunCommand("rm", "-rf", venv); err != nil {
return "", errors.Wrap(err, "remove existed venv failed")
}
}
// create python3 .venv
if err := RunCommand("python3", "-m", "venv", venv); err != nil {
return "", errors.Wrap(err, "create python3 venv failed")
}
}
// install default python packages
for _, pkg := range packages {
err := InstallPythonPackage(python3, pkg)
if err != nil {
return "", errors.Wrap(err, fmt.Sprintf("pip install %s failed", pkg))
}
}
return python3, nil
}
func Command(name string, arg ...string) *exec.Cmd {
cmd := exec.Command(name, arg...)
cmd.SysProcAttr = &syscall.SysProcAttr{
Setpgid: true,
}
return cmd
}
func KillProcessesByGpid(cmd *exec.Cmd) error {
return syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
}

View File

@@ -1,108 +0,0 @@
//go:build windows
package myexec
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"syscall"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
)
func init() {
// use python instead of python3 if python3 is not available
if !isPython3(python3Executable) {
python3Executable = "python"
}
}
func getPython3Executable(venvDir string) string {
python := filepath.Join(venvDir, "Scripts", "python3.exe")
if isPython3(python) {
return python
}
return filepath.Join(venvDir, "Scripts", "python.exe")
}
func ensurePython3Venv(venvDir string, packages ...string) (python3 string, err error) {
python3 = getPython3Executable(venvDir)
log.Info().
Str("python3", python3).
Interface("packages", packages).
Msg("ensure python3 venv")
systemPython := "python3"
// check if python3 venv is available
if !isPython3(python3) {
// python3 venv not available, create one
// check if system python3 is available
log.Warn().Str("pythonPath", python3).Msg("python3 venv is not available, try to check system python3")
if !isPython3(systemPython) {
if !isPython3("python") {
return "", errors.Wrap(err, "python3 not found")
}
systemPython = "python"
}
// check if .venv exists
if _, err := os.Stat(venvDir); err == nil {
// .venv exists, remove first
if err := RunCommand("del", "/q", venvDir); err != nil {
return "", errors.Wrap(err, "remove existed venv failed")
}
}
// create python3 .venv
// notice: --symlinks should be specified for windows
// https://github.com/actions/virtual-environments/issues/2690
if err := RunCommand(systemPython, "-m", "venv", "--symlinks", venvDir); err != nil {
// fix: failed to symlink on Windows
log.Warn().Msg("failed to create python3 .venv by using --symlinks, try to use --copies")
if err := RunCommand(systemPython, "-m", "venv", "--copies", venvDir); err != nil {
return "", errors.Wrap(err, "create python3 venv failed")
}
}
// fix: python3 doesn't exist in .venv on Windows
if _, err := os.Stat(python3); err != nil {
log.Warn().Msg("python3 doesn't exist, try to link python")
err := os.Link(filepath.Join(venvDir, "Scripts", "python.exe"), python3)
if err != nil {
return "", errors.Wrap(err, "python3 doesn't exist in .venv")
}
}
}
// install default python packages
for _, pkg := range packages {
err := InstallPythonPackage(python3, pkg)
if err != nil {
return "", errors.Wrap(err, fmt.Sprintf("pip install %s failed", pkg))
}
}
return python3, nil
}
func Command(name string, arg ...string) *exec.Cmd {
// "cmd /c" carries out the command specified by string and then stops
// refer: https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/cmd
cmd := exec.Command("cmd.exe")
cmd.SysProcAttr = &syscall.SysProcAttr{
CmdLine: strings.Join(append([]string{"/c", name}, arg...), " "),
HideWindow: true,
}
return cmd
}
func KillProcessesByGpid(cmd *exec.Cmd) error {
killCmd := Command("taskkill", "/T", "/F", "/PID ", strconv.Itoa(cmd.Process.Pid))
return killCmd.Run()
}

View File

@@ -1,7 +1,7 @@
package pytest
import (
"github.com/httprunner/httprunner/v4/hrp/internal/myexec"
"github.com/httprunner/funplugin/myexec"
)
func RunPytest(args []string) error {

View File

@@ -9,26 +9,26 @@ func TestGenDemoExamples(t *testing.T) {
dir := "../../../examples/demo-with-go-plugin"
err := CreateScaffold(dir, Go, "", true)
if err != nil {
t.Fatal()
t.Fatal(err)
}
// FIXME
dir = "../../../examples/demo-with-py-plugin"
venv := filepath.Join(dir, ".venv")
err = CreateScaffold(dir, Py, venv, true)
if err != nil {
t.Fatal()
}
_ = CreateScaffold(dir, Py, venv, true)
// FIXME
// if err != nil {
// t.Fatal(err)
// }
dir = "../../../examples/demo-without-plugin"
err = CreateScaffold(dir, Ignore, "", true)
if err != nil {
t.Fatal()
t.Fatal(err)
}
dir = "../../../examples/demo-empty-project"
err = CreateScaffold(dir, Empty, "", true)
if err != nil {
t.Fatal()
t.Fatal(err)
}
}

View File

@@ -7,14 +7,14 @@ import (
"path/filepath"
"time"
"github.com/httprunner/funplugin/fungo"
"github.com/httprunner/funplugin/myexec"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
"github.com/httprunner/httprunner/v4/hrp"
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
"github.com/httprunner/httprunner/v4/hrp/internal/code"
"github.com/httprunner/httprunner/v4/hrp/internal/env"
"github.com/httprunner/httprunner/v4/hrp/internal/myexec"
"github.com/httprunner/httprunner/v4/hrp/internal/sdk"
"github.com/httprunner/httprunner/v4/hrp/internal/version"
)
@@ -210,13 +210,10 @@ func createPythonPlugin(projectName, venv string) error {
return errors.Wrap(err, "copy file failed")
}
packages := []string{
fmt.Sprintf("funppy==%s", fungo.Version),
fmt.Sprintf("httprunner==%s", version.HttpRunnerMinimumVersion),
}
packages := []string{"funppy", "httprunner"}
_, err = myexec.EnsurePython3Venv(venv, packages...)
if err != nil {
return err
return errors.Wrap(code.InvalidPython3Venv, err.Error())
}
return nil

View File

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

View File

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

View File

@@ -1 +1 @@
v4.3.6-beta
v4.3.6

View File

@@ -6,6 +6,3 @@ import (
//go:embed VERSION
var VERSION string
// httprunner python version
const HttpRunnerMinimumVersion = "v4.3.5"

View File

@@ -1,9 +1,8 @@
package wiki
import (
"github.com/httprunner/funplugin/myexec"
"github.com/rs/zerolog/log"
"github.com/httprunner/httprunner/v4/hrp/internal/myexec"
)
func OpenWiki() error {

View File

@@ -4,10 +4,10 @@ import (
_ "embed"
"os"
"github.com/httprunner/funplugin/myexec"
"github.com/rs/zerolog/log"
"github.com/httprunner/httprunner/v4/hrp"
"github.com/httprunner/httprunner/v4/hrp/internal/myexec"
)
func convert2GoTestScripts(paths ...string) error {

View File

@@ -1,7 +1,6 @@
package convert
import (
_ "embed"
"path/filepath"
"time"

View File

@@ -1,9 +1,8 @@
package convert
import (
"github.com/httprunner/funplugin/myexec"
"github.com/pkg/errors"
"github.com/httprunner/httprunner/v4/hrp/internal/myexec"
)
// convert TCase to pytest case

View File

@@ -273,7 +273,7 @@ func (d *Device) RunShellCommandWithBytes(cmd string, args ...string) ([]byte, e
// log elapsed seconds for shell execution
log.Debug().Str("cmd",
fmt.Sprintf("adb -s %s shell %s", d.serial, cmd)).
Float64("elapsed(s)", time.Since(startTime).Seconds()).
Int64("elapsed(ms)", time.Since(startTime).Milliseconds()).
Msg("run adb shell")
}()
@@ -295,7 +295,7 @@ func (d *Device) RunShellCommandV2WithBytes(cmd string, args ...string) ([]byte,
// log elapsed seconds for shell execution
log.Debug().Str("cmd",
fmt.Sprintf("adb -s %s shell %s", d.serial, cmd)).
Float64("elapsed(s)", time.Since(startTime).Seconds()).
Int64("elapsed(ms)", time.Since(startTime).Milliseconds()).
Msg("run adb shell in v2")
}()

View File

@@ -3,7 +3,6 @@ package uixt
import (
"encoding/json"
"fmt"
"math"
"math/rand"
"time"
@@ -112,6 +111,12 @@ type ActionOptions struct {
// set custiom options such as textview, id, description
Custom map[string]interface{} `json:"custom,omitempty" yaml:"custom,omitempty"`
// screenshot related
ScreenShotWithOCR bool `json:"screenshot_with_ocr,omitempty" yaml:"screenshot_with_ocr,omitempty"`
ScreenShotWithUpload bool `json:"screenshot_with_upload,omitempty" yaml:"screenshot_with_upload,omitempty"`
ScreenShotWithLiveType bool `json:"screenshot_with_live_type,omitempty" yaml:"screenshot_with_live_type,omitempty"`
ScreenShotWithUITypes []string `json:"screenshot_with_ui_types,omitempty" yaml:"screenshot_with_ui_types,omitempty"`
}
func (o *ActionOptions) Options() []ActionOption {
@@ -186,9 +191,41 @@ func (o *ActionOptions) Options() []ActionOption {
}
}
// screenshot options
if o.ScreenShotWithOCR {
options = append(options, WithScreenShotOCR(true))
}
if o.ScreenShotWithUpload {
options = append(options, WithScreenShotUpload(true))
}
if o.ScreenShotWithLiveType {
options = append(options, WithScreenShotLiveType(true))
}
if len(o.ScreenShotWithUITypes) > 0 {
options = append(options, WithScreenShotUITypes(o.ScreenShotWithUITypes...))
}
return options
}
func (o *ActionOptions) screenshotActions() []string {
actions := []string{}
if o.ScreenShotWithOCR {
actions = append(actions, "ocr")
}
if o.ScreenShotWithUpload {
actions = append(actions, "upload")
}
if o.ScreenShotWithLiveType {
actions = append(actions, "liveType")
}
// UI detection
if len(o.ScreenShotWithUITypes) > 0 {
actions = append(actions, "ui")
}
return actions
}
func NewActionOptions(options ...ActionOption) *ActionOptions {
actionOptions := &ActionOptions{}
for _, option := range options {
@@ -364,6 +401,30 @@ func WithIgnoreNotFoundError(ignoreError bool) ActionOption {
}
}
func WithScreenShotOCR(ocrOn bool) ActionOption {
return func(o *ActionOptions) {
o.ScreenShotWithOCR = ocrOn
}
}
func WithScreenShotUpload(uploadOn bool) ActionOption {
return func(o *ActionOptions) {
o.ScreenShotWithUpload = uploadOn
}
}
func WithScreenShotLiveType(liveTypeOn bool) ActionOption {
return func(o *ActionOptions) {
o.ScreenShotWithLiveType = liveTypeOn
}
}
func WithScreenShotUITypes(uiTypes ...string) ActionOption {
return func(o *ActionOptions) {
o.ScreenShotWithUITypes = uiTypes
}
}
func (dExt *DriverExt) ParseActionOptions(options ...ActionOption) []ActionOption {
actionOptions := NewActionOptions(options...)
@@ -386,7 +447,7 @@ func (dExt *DriverExt) GenAbsScope(x1, y1, x2, y2 float64) AbsScope {
return AbsScope{absX1, absY1, absX2, absY2}
}
func (dExt *DriverExt) DoAction(action MobileAction) error {
func (dExt *DriverExt) DoAction(action MobileAction) (err error) {
log.Debug().
Str("method", string(action.Method)).
Interface("params", action.Params).
@@ -394,11 +455,19 @@ func (dExt *DriverExt) DoAction(action MobileAction) error {
actionStartTime := time.Now()
defer func() {
log.Debug().
Str("method", string(action.Method)).
Interface("params", action.Params).
Float64("elapsed(s)", time.Since(actionStartTime).Seconds()).
Msg("uixt action end")
if err != nil {
log.Error().Err(err).
Str("method", string(action.Method)).
Interface("params", action.Params).
Int64("elapsed(ms)", time.Since(actionStartTime).Milliseconds()).
Msg("uixt action end")
} else {
log.Debug().
Str("method", string(action.Method)).
Interface("params", action.Params).
Int64("elapsed(ms)", time.Since(actionStartTime).Milliseconds()).
Msg("uixt action end")
}
}()
switch action.Method {
@@ -533,13 +602,14 @@ func (dExt *DriverExt) DoAction(action MobileAction) error {
return fmt.Errorf("invalid sleep params: %v(%T)", action.Params, action.Params)
case ACTION_SleepRandom:
if params, ok := action.Params.([]interface{}); ok {
return sleepRandom(time.Now(), params)
sleepStrict(time.Now(), getSimulationDuration(params))
return nil
}
return fmt.Errorf("invalid sleep random params: %v(%T)", action.Params, action.Params)
case ACTION_ScreenShot:
// take screenshot
log.Info().Msg("take screenshot for current screen")
_, _, err := dExt.takeScreenShot(builtin.GenNameWithTimestamp("%d_screenshot"))
_, err := dExt.GetScreenResult(action.GetOptions()...)
return err
case ACTION_StartCamera:
return dExt.Driver.StartCamera()
@@ -575,13 +645,20 @@ func convertToFloat64(val interface{}) (float64, error) {
}
}
// sleepRandom sleeps random time with given params
// startTime is used to correct sleep duration caused by process time
func sleepRandom(startTime time.Time, params []interface{}) error {
// getSimulationDuration returns simulation duration by given params (in seconds)
func getSimulationDuration(params []interface{}) (milliseconds int64) {
if len(params) == 1 {
// constant sleep time
params = append(params, params[0], 1.0)
} else if len(params) == 2 {
// given constant duration time
seconds, err := convertToFloat64(params[0])
if err != nil {
log.Error().Err(err).Interface("params", params).Msg("invalid params")
return 0
}
return int64(seconds * 1000)
}
if len(params) == 2 {
// given [min, max], missing weight
// append default weight 1
params = append(params, 1.0)
}
@@ -593,15 +670,18 @@ func sleepRandom(startTime time.Time, params []interface{}) error {
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])
log.Error().Err(err).Interface("min", params[i]).Msg("invalid minimum time")
return 0
}
max, err := convertToFloat64(params[i+1])
if err != nil {
return errors.Wrapf(err, "invalid maximum time: %v", params[i+1])
log.Error().Err(err).Interface("max", params[i+1]).Msg("invalid maximum time")
return 0
}
weight, err := convertToFloat64(params[i+2])
if err != nil {
return errors.Wrapf(err, "invalid weight value: %v", params[i+2])
log.Error().Err(err).Interface("weight", params[i+2]).Msg("invalid weight value")
return 0
}
totalProb += weight
sections = append(sections,
@@ -610,8 +690,8 @@ func sleepRandom(startTime time.Time, params []interface{}) error {
}
if totalProb == 0 {
log.Warn().Msg("total weight is 0, skip sleep")
return nil
log.Warn().Msg("total weight is 0, skip simulation")
return 0
}
r := rand.Float64()
@@ -619,22 +699,36 @@ func sleepRandom(startTime time.Time, params []interface{}) error {
for _, s := range sections {
accProb += s.weight / totalProb
if r < accProb {
elapsed := time.Since(startTime).Seconds()
randomSeconds := s.min + rand.Float64()*(s.max-s.min)
dur := randomSeconds - elapsed
// if elapsed time is greater than random seconds, skip sleep to reduce deviation caused by process time
if dur <= 0 {
log.Info().Float64("elapsed", elapsed).Float64("randomSeconds", randomSeconds).
Interface("strategy_params", params).Msg("elapsed duration >= random seconds, skip sleep")
} else {
log.Info().Float64("sleepDuration", dur).Float64("elapsed", elapsed).Float64("randomSeconds", randomSeconds).
Interface("strategy_params", params).Msg("sleep remaining random seconds")
time.Sleep(time.Duration(math.Ceil(dur*1000)) * time.Millisecond)
}
return nil
milliseconds := int64((s.min + rand.Float64()*(s.max-s.min)) * 1000)
log.Info().Int64("random(ms)", milliseconds).
Interface("strategy_params", params).Msg("get simulation duration")
return milliseconds
}
}
return nil
log.Warn().Interface("strategy_params", params).
Msg("get simulation duration failed, skip simulation")
return 0
}
// sleepStrict sleeps strict duration with given params
// startTime is used to correct sleep duration caused by process time
func sleepStrict(startTime time.Time, strictMilliseconds int64) {
elapsed := time.Since(startTime).Milliseconds()
dur := strictMilliseconds - elapsed
// if elapsed time is greater than given duration, skip sleep to reduce deviation caused by process time
if dur <= 0 {
log.Warn().
Int64("elapsed(ms)", elapsed).
Int64("strictSleep(ms)", strictMilliseconds).
Msg("elapsed >= simulation duration, skip sleep")
return
}
log.Info().Int64("sleepDuration(ms)", dur).
Int64("elapsed(ms)", elapsed).
Int64("strictSleep(ms)", strictMilliseconds).
Msg("sleep remaining duration time")
time.Sleep(time.Duration(dur) * time.Millisecond)
}

View File

@@ -15,33 +15,32 @@ func checkErr(t *testing.T, err error, msg ...string) {
}
}
func TestSleepRandom(t *testing.T) {
startTime1 := time.Now()
params := []interface{}{1}
err := sleepRandom(startTime1, params)
checkErr(t, err)
dur := time.Since(startTime1).Seconds()
t.Log(dur)
if dur < 1 || dur > 1.1 {
t.Fatal("sleepRandom failed")
func TestGetSimulationDuration(t *testing.T) {
params := []interface{}{1.23}
duration := getSimulationDuration(params)
if duration != 1230 {
t.Fatal("getSimulationDuration failed")
}
params = []interface{}{0, 2}
err = sleepRandom(startTime1, params)
checkErr(t, err)
dur = time.Since(startTime1).Seconds()
t.Log(dur)
if dur < 1 || dur > 2 {
t.Fatal("sleepRandom failed")
}
startTime2 := time.Now()
params = []interface{}{1, 2}
err = sleepRandom(startTime2, params)
checkErr(t, err)
dur = time.Since(startTime2).Seconds()
t.Log(dur)
if dur < 1 || dur > 2 {
t.Fatal("sleepRandom failed")
duration = getSimulationDuration(params)
if duration < 1000 || duration > 2000 {
t.Fatal("getSimulationDuration failed")
}
params = []interface{}{1, 5, 0.7, 5, 10, 0.3}
duration = getSimulationDuration(params)
if duration < 1000 || duration > 10000 {
t.Fatal("getSimulationDuration failed")
}
}
func TestSleepStrict(t *testing.T) {
startTime := time.Now()
sleepStrict(startTime, 1230)
dur := time.Since(startTime).Milliseconds()
t.Log(dur)
if dur < 1230 || dur > 1300 {
t.Fatalf("sleepRandom failed, dur: %d", dur)
}
}

View File

@@ -8,12 +8,12 @@ import (
"os/exec"
"strings"
"github.com/httprunner/funplugin/myexec"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
"github.com/httprunner/httprunner/v4/hrp/internal/code"
"github.com/httprunner/httprunner/v4/hrp/internal/json"
"github.com/httprunner/httprunner/v4/hrp/internal/myexec"
"github.com/httprunner/httprunner/v4/hrp/pkg/gadb"
)
@@ -154,10 +154,15 @@ func (dev *AndroidDevice) LogEnabled() bool {
return dev.LogOn
}
func (dev *AndroidDevice) NewDriver(capabilities Capabilities) (driverExt *DriverExt, err error) {
func (dev *AndroidDevice) NewDriver(options ...DriverOption) (driverExt *DriverExt, err error) {
driverOptions := &DriverOptions{}
for _, option := range options {
option(driverOptions)
}
var driver WebDriver
if dev.UIA2 {
driver, err = dev.NewUSBDriver(capabilities)
driver, err = dev.NewUSBDriver(driverOptions.capabilities)
} else {
driver, err = dev.NewAdbDriver()
}
@@ -165,15 +170,10 @@ func (dev *AndroidDevice) NewDriver(capabilities Capabilities) (driverExt *Drive
return nil, errors.Wrap(err, "failed to init UIA driver")
}
driverExt, err = NewDriverExt(dev, driver)
driverExt, err = newDriverExt(dev, driver, driverOptions.plugin)
if err != nil {
return nil, err
}
err = driverExt.extendCV()
if err != nil {
return nil, errors.Wrap(code.MobileUIDriverError,
fmt.Sprintf("extend OpenCV failed: %v", err))
}
if dev.LogOn {
err = driverExt.Driver.StartCaptureLog("hrp_adb_log")

View File

@@ -18,7 +18,7 @@ var (
func setupAndroid(t *testing.T) {
device, err := NewAndroidDevice()
checkErr(t, err)
driverExt, err = device.NewDriver(nil)
driverExt, err = device.NewDriver()
checkErr(t, err)
}
@@ -331,7 +331,7 @@ func TestDeviceList(t *testing.T) {
func TestDriver_AppLaunch(t *testing.T) {
device, _ := NewAndroidDevice()
driver, err := device.NewDriver(nil)
driver, err := device.NewDriver()
if err != nil {
t.Fatal(err)
}
@@ -383,7 +383,7 @@ func TestDriver_IsAppInForeground(t *testing.T) {
func TestDriver_KeepAlive(t *testing.T) {
device, _ := NewAndroidDevice()
driver, err := device.NewDriver(nil)
driver, err := device.NewDriver()
if err != nil {
t.Fatal(err)
}
@@ -408,7 +408,7 @@ func TestDriver_KeepAlive(t *testing.T) {
func TestDriver_AppTerminate(t *testing.T) {
device, _ := NewAndroidDevice()
driver, err := device.NewDriver(nil)
driver, err := device.NewDriver()
if err != nil {
t.Fatal(err)
}

View File

@@ -22,7 +22,7 @@ func TestIOSDemo(t *testing.T) {
capabilities := uixt.NewCapabilities()
capabilities.WithDefaultAlertAction(uixt.AlertActionAccept) // or uixt.AlertActionDismiss
driverExt, err := device.NewDriver(capabilities)
driverExt, err := device.NewDriver(uixt.WithDriverCapabilities(capabilities))
if err != nil {
t.Fatal(err)
}

View File

@@ -7,7 +7,7 @@ import (
)
func TestDriverExt_Drag(t *testing.T) {
driverExt, err := iosDevice.NewDriver(nil)
driverExt, err := iosDevice.NewDriver()
checkErr(t, err)
pathSearch := "/Users/hero/Documents/temp/2020-05/opencv/IMG_map.png"

View File

@@ -19,10 +19,12 @@ import (
"syscall"
"time"
"github.com/httprunner/funplugin"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
"github.com/httprunner/httprunner/v4/hrp/internal/code"
"github.com/httprunner/httprunner/v4/hrp/internal/env"
)
@@ -48,35 +50,41 @@ func WithThreshold(threshold float64) CVOption {
}
}
type Popularity struct {
Stars string `json:"stars,omitempty"` // 点赞数
Comments string `json:"comments,omitempty"` // 评论数
Favorites string `json:"favorites,omitempty"` // 收藏数
Shares string `json:"shares,omitempty"` // 分享数
LiveUsers string `json:"live_users,omitempty"` // 直播间人数
}
type ScreenResult struct {
Texts OCRTexts `json:"texts"` // dumped OCRTexts
Tags []string `json:"tags"` // tags for image, e.g. ["feed", "ad", "live"]
Popularity Popularity `json:"popularity"` // video popularity data
bufSource *bytes.Buffer // raw image buffer bytes
imagePath string // image file path
UploadedURL string `json:"uploaded_url"` // uploaded image url
Texts OCRTexts `json:"texts"` // dumped raw OCRTexts
Icons UIResultMap `json:"icons"` // CV 识别的图标
Tags []string `json:"tags"` // tags for image, e.g. ["feed", "ad", "live"]
VideoType string `json:"video_type,omitempty"` // video type: feed, live-preview or live
Feed *FeedVideo `json:"feed,omitempty"`
Live *LiveRoom `json:"live,omitempty"`
SwipeStartTime int64 `json:"swipe_start_time"` // 滑动开始时间戳
SwipeFinishTime int64 `json:"swipe_finish_time"` // 滑动结束时间戳
ScreenshotTakeElapsed int64 `json:"screenshot_take_elapsed"` // 设备截图耗时(ms)
ScreenshotCVElapsed int64 `json:"screenshot_cv_elapsed"` // CV 识别耗时(ms)
// 当前 Feed/Live 整体耗时
TotalElapsed int64 `json:"total_elapsed"` // current_swipe_finish -> next_swipe_start 整体耗时(ms)
}
type cacheStepData struct {
// cache step screenshot paths
screenShots []string
screenShotsUrls map[string]string // map screenshot file path to uploaded url
screenShots []string
// cache step screenshot ocr results, key is image path, value is ScreenResult
screenResults map[string]*ScreenResult
// cache feed/live video stat
videoStat *VideoStat
videoCrawler *VideoCrawler
}
func (d *cacheStepData) reset() {
d.screenShots = make([]string, 0)
d.screenShotsUrls = make(map[string]string)
d.screenResults = make(map[string]*ScreenResult)
d.videoStat = nil
d.videoCrawler = nil
}
type DriverExt struct {
@@ -91,15 +99,26 @@ type DriverExt struct {
// cache step data
cacheStepData cacheStepData
// funplugin
plugin funplugin.IPlugin
}
func NewDriverExt(device Device, driver WebDriver) (dExt *DriverExt, err error) {
func newDriverExt(device Device, driver WebDriver, plugin funplugin.IPlugin) (dExt *DriverExt, err error) {
dExt = &DriverExt{
Device: device,
Driver: driver,
plugin: plugin,
cacheStepData: cacheStepData{},
interruptSignal: make(chan os.Signal, 1),
}
err = dExt.extendCV()
if err != nil {
return nil, errors.Wrap(code.MobileUIDriverError,
fmt.Sprintf("extend OpenCV failed: %v", err))
}
dExt.cacheStepData.reset()
signal.Notify(dExt.interruptSignal, syscall.SIGTERM, syscall.SIGINT)
dExt.doneMjpegStream = make(chan bool, 1)
@@ -201,9 +220,17 @@ func (dExt *DriverExt) saveScreenShot(raw *bytes.Buffer, fileName string) (strin
func (dExt *DriverExt) GetStepCacheData() map[string]interface{} {
cacheData := make(map[string]interface{})
cacheData["video_stat"] = dExt.cacheStepData.videoStat
cacheData["video_stat"] = dExt.cacheStepData.videoCrawler
cacheData["screenshots"] = dExt.cacheStepData.screenShots
cacheData["screenshots_urls"] = dExt.cacheStepData.screenShotsUrls
screenShotsUrls := make(map[string]string)
for imagePath, screenResult := range dExt.cacheStepData.screenResults {
if screenResult.UploadedURL == "" {
continue
}
screenShotsUrls[imagePath] = screenResult.UploadedURL
}
cacheData["screenshots_urls"] = screenShotsUrls
screenSize, err := dExt.Driver.WindowSize()
if err != nil {
@@ -214,13 +241,20 @@ func (dExt *DriverExt) GetStepCacheData() map[string]interface{} {
for imagePath, screenResult := range dExt.cacheStepData.screenResults {
o, _ := json.Marshal(screenResult.Texts)
data := map[string]interface{}{
"tags": screenResult.Tags,
"texts": string(o),
"popularity": screenResult.Popularity,
"tags": screenResult.Tags,
"texts": string(o),
"resolution": map[string]int{
"width": screenSize.Width,
"height": screenSize.Height,
},
"video_type": screenResult.VideoType,
"feed": screenResult.Feed,
"live": screenResult.Live,
"swipe_start_time": screenResult.SwipeStartTime,
"swipe_finish_time": screenResult.SwipeFinishTime,
"screenshot_take_elapsed": screenResult.ScreenshotTakeElapsed,
"screenshot_cv_elapsed": screenResult.ScreenshotCVElapsed,
"total_elapsed": screenResult.TotalElapsed,
}
screenResults[imagePath] = data

View File

@@ -4,6 +4,8 @@ import (
"bytes"
"strings"
"time"
"github.com/httprunner/funplugin"
)
var (
@@ -437,11 +439,30 @@ type Rect struct {
Size
}
type DriverOptions struct {
capabilities Capabilities
plugin funplugin.IPlugin
}
type DriverOption func(*DriverOptions)
func WithDriverCapabilities(capabilities Capabilities) DriverOption {
return func(options *DriverOptions) {
options.capabilities = capabilities
}
}
func WithDriverPlugin(plugin funplugin.IPlugin) DriverOption {
return func(options *DriverOptions) {
options.plugin = plugin
}
}
// current implemeted device: IOSDevice, AndroidDevice
type Device interface {
UUID() string // ios udid or android serial
LogEnabled() bool
NewDriver(capabilities Capabilities) (driverExt *DriverExt, err error)
NewDriver(...DriverOption) (driverExt *DriverExt, err error)
StartPerf() error
StopPerf() string

View File

@@ -290,8 +290,14 @@ func (dev *IOSDevice) LogEnabled() bool {
return dev.LogOn
}
func (dev *IOSDevice) NewDriver(capabilities Capabilities) (driverExt *DriverExt, err error) {
func (dev *IOSDevice) NewDriver(options ...DriverOption) (driverExt *DriverExt, err error) {
driverOptions := &DriverOptions{}
for _, option := range options {
option(driverOptions)
}
// init WDA driver
capabilities := driverOptions.capabilities
if capabilities == nil {
capabilities = NewCapabilities()
capabilities.WithDefaultAlertAction(AlertActionAccept)
@@ -316,15 +322,11 @@ func (dev *IOSDevice) NewDriver(capabilities Capabilities) (driverExt *DriverExt
}
}
driverExt, err = NewDriverExt(dev, driver)
driverExt, err = newDriverExt(dev, driver, driverOptions.plugin)
if err != nil {
return nil, err
}
err = driverExt.extendCV()
if err != nil {
return nil, errors.Wrap(code.MobileUIDriverError,
fmt.Sprintf("extend OpenCV failed: %v", err))
}
settings, err := driverExt.Driver.SetAppiumSettings(map[string]interface{}{
"snapshotMaxDepth": dev.SnapshotMaxDepth,
"acceptAlertButtonSelector": dev.AcceptAlertButtonSelector,

View File

@@ -14,8 +14,6 @@ import (
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
"gocv.io/x/gocv"
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
)
const (
@@ -101,9 +99,11 @@ func (dExt *DriverExt) FindAllImageRect(search string) (rects []image.Rectangle,
if bufSearch, err = getBufFromDisk(search); err != nil {
return nil, err
}
if bufSource, _, err = dExt.takeScreenShot(builtin.GenNameWithTimestamp("%d_cv")); err != nil {
screenResult, err := dExt.GetScreenResult()
if err != nil {
return nil, err
}
bufSource = screenResult.bufSource
if rects, err = FindAllImageRectsFromRaw(bufSource, bufSearch, float32(dExt.threshold), TemplateMatchMode(dExt.matchMode)); err != nil {
return nil, err

View File

@@ -24,7 +24,7 @@ var popups = [][]string{
{"管理使用时间", ".*忽略.*"},
}
func (dExt *DriverExt) AutoPopupHandler(screenTexts OCRTexts) error {
func findTextPopup(screenTexts OCRTexts) (closePoint *OCRText) {
for _, popup := range popups {
if len(popup) != 2 {
continue
@@ -34,17 +34,39 @@ func (dExt *DriverExt) AutoPopupHandler(screenTexts OCRTexts) error {
if err == nil {
log.Warn().Interface("popup", popup).
Interface("texts", screenTexts).Msg("text popup found")
point := points[1].Center()
log.Info().Str("text", points[1].Text).Msg("close popup")
if err := dExt.TapAbsXY(point.X, point.Y); err != nil {
log.Error().Err(err).Msg("tap popup failed")
return errors.Wrap(code.MobileUIPopupError, err.Error())
}
// tap popup success
return nil
closePoint = &points[1]
break
}
}
return
}
// no popup found
func (dExt *DriverExt) handleTextPopup(screenTexts OCRTexts) error {
closePoint := findTextPopup(screenTexts)
if closePoint == nil {
// no popup found
return nil
}
log.Info().Str("text", closePoint.Text).Msg("close popup")
pointCenter := closePoint.Center()
if err := dExt.TapAbsXY(pointCenter.X, pointCenter.Y); err != nil {
log.Error().Err(err).Msg("tap popup failed")
return errors.Wrap(code.MobileUIPopupError, err.Error())
}
// tap popup success
return nil
}
func (dExt *DriverExt) AutoPopupHandler() error {
// TODO: check popup by activity type
// check popup by screenshot
screenResult, err := dExt.GetScreenResult(
WithScreenShotOCR(true), WithScreenShotUpload(true))
if err != nil {
return errors.Wrap(err, "get screen result failed for popup handler")
}
return dExt.handleTextPopup(screenResult.Texts)
}

View File

@@ -53,11 +53,20 @@ func (o OCRResults) ToOCRTexts() (ocrTexts OCRTexts) {
}
type ImageResult struct {
imagePath string
URL string `json:"url"` // image uploaded url
OCRResult OCRResults `json:"ocrResult"` // OCR texts
LiveType string `json:"liveType"` // 直播间类型
UIResult UIResultMap `json:"uiResult"` // 图标检测
URL string `json:"url"` // image uploaded url
OCRResult OCRResults `json:"ocrResult"` // OCR texts
// NoLive非直播间
// Shop电商
// LifeService生活服务
// Show秀场
// Game游戏
// People多人
// PKPK
// Media媒体
// Chat语音
// Event赛事
LiveType string `json:"liveType"` // 直播间类型
UIResult UIResultMap `json:"uiResult"` // 图标检测
}
type APIResponseImage struct {
@@ -170,35 +179,31 @@ func newVEDEMImageService() (*veDEMImageService, error) {
// veDEMImageService implements IImageService interface
// actions:
//
// ocr - get ocr texts
// upload - get image uploaded url
// liveType - get live type
// popup - get popup windows
// close - get close popup
// ui - get ui position by type(s)
// ocr - get ocr texts
// upload - get image uploaded url
// liveType - get live type
// popup - get popup windows
// close - get close popup
// ui - get ui position by type(s)
type veDEMImageService struct{}
type (
actionOptions []string
uiTypeOptions []string
)
func (s *veDEMImageService) GetImage(imageBuf *bytes.Buffer, options ...interface{}) (imageResult ImageResult, err error) {
func (s *veDEMImageService) GetImage(imageBuf *bytes.Buffer, options ...ActionOption) (imageResult *ImageResult, err error) {
actionOptions := NewActionOptions(options...)
screenshotActions := actionOptions.screenshotActions()
if len(screenshotActions) == 0 {
// skip
return nil, nil
}
bodyBuf := &bytes.Buffer{}
bodyWriter := multipart.NewWriter(bodyBuf)
for _, option := range options {
switch ov := option.(type) {
case actionOptions:
for _, action := range ov {
bodyWriter.WriteField("actions", action)
}
case uiTypeOptions:
for _, uiType := range ov {
bodyWriter.WriteField("uiTypes", uiType)
}
default:
log.Warn().Interface("option", ov).Msgf("unexpected image service option")
}
for _, action := range screenshotActions {
bodyWriter.WriteField("actions", action)
}
for _, uiType := range actionOptions.ScreenShotWithUITypes {
bodyWriter.WriteField("uiTypes", uiType)
}
bodyWriter.WriteField("ocrCluster", "highPrecision")
formWriter, err := bodyWriter.CreateFormFile("image", "screenshot.png")
@@ -256,7 +261,7 @@ func (s *veDEMImageService) GetImage(imageBuf *bytes.Buffer, options ...interfac
log.Debug().
Str("X-TT-LOGID", logID).
Int("image_bytes", size).
Float64("elapsed(s)", elapsed.Seconds()).
Int64("elapsed(ms)", elapsed.Milliseconds()).
Msg("request OCR service success")
break
}
@@ -305,7 +310,7 @@ func (s *veDEMImageService) GetImage(imageBuf *bytes.Buffer, options ...interfac
Msg("request veDEM OCR service failed")
}
imageResult = imageResponse.Result
imageResult = &imageResponse.Result
log.Debug().Interface("imageResult", imageResult).Msg("get image data by veDEM")
return imageResult, nil
}
@@ -338,46 +343,58 @@ func getLogID(header http.Header) string {
type IImageService interface {
// GetImage returns image result including ocr texts, uploaded image url, etc
GetImage(imageBuf *bytes.Buffer, options ...interface{}) (imageResult ImageResult, err error)
GetImage(imageBuf *bytes.Buffer, options ...ActionOption) (imageResult *ImageResult, err error)
}
// GetScreenResult takes a screenshot, returns the image recognization result
func (dExt *DriverExt) GetScreenResult() (screenResult *ScreenResult, err error) {
var bufSource *bytes.Buffer
var imagePath string
if bufSource, imagePath, err = dExt.takeScreenShot(
builtin.GenNameWithTimestamp("%d_cv")); err != nil {
return
}
imageResult, err := dExt.ImageService.GetImage(bufSource, actionOptions{"ocr", "upload", "liveType"})
func (dExt *DriverExt) GetScreenResult(options ...ActionOption) (screenResult *ScreenResult, err error) {
startTime := time.Now()
fileName := builtin.GenNameWithTimestamp("%d_screenshot")
bufSource, imagePath, err := dExt.takeScreenShot(fileName)
if err != nil {
log.Error().Err(err).Msg("GetImage from ImageService failed")
return
}
imageResult.imagePath = imagePath
imageUrl := imageResult.URL
if imageUrl != "" {
dExt.cacheStepData.screenShotsUrls[imagePath] = imageUrl
log.Debug().Str("imagePath", imagePath).Str("imageUrl", imageUrl).Msg("log screenshot")
}
screenshotTakeElapsed := time.Since(startTime).Milliseconds()
screenResult = &ScreenResult{
Texts: imageResult.OCRResult.ToOCRTexts(),
Tags: nil,
Popularity: Popularity{},
bufSource: bufSource,
imagePath: imagePath,
Tags: nil,
ScreenshotTakeElapsed: screenshotTakeElapsed,
}
if imageResult.LiveType != "" {
screenResult.Tags = []string{imageResult.LiveType}
imageResult, err := dExt.ImageService.GetImage(bufSource, options...)
if err != nil {
log.Error().Err(err).Msg("GetImage from ImageService failed")
return nil, err
}
if imageResult != nil {
screenResult.ScreenshotCVElapsed = time.Since(startTime).Milliseconds() - screenshotTakeElapsed
screenResult.Texts = imageResult.OCRResult.ToOCRTexts()
screenResult.UploadedURL = imageResult.URL
screenResult.Icons = imageResult.UIResult
if imageResult.LiveType != "" && imageResult.LiveType != "NoLive" {
screenResult.Live = &LiveRoom{
LiveType: imageResult.LiveType,
}
}
}
dExt.cacheStepData.screenResults[imagePath] = screenResult
log.Debug().
Str("imagePath", imagePath).
Str("imageUrl", screenResult.UploadedURL).
Int64("screenshot_take_elapsed(ms)", screenResult.ScreenshotTakeElapsed).
Int64("screenshot_cv_elapsed(ms)", screenResult.ScreenshotCVElapsed).
Msg("log screenshot")
return screenResult, nil
}
func (dExt *DriverExt) GetScreenTexts() (ocrTexts OCRTexts, err error) {
screenResult, err := dExt.GetScreenResult()
screenResult, err := dExt.GetScreenResult(
WithScreenShotOCR(true), WithScreenShotUpload(true))
if err != nil {
return
}
@@ -495,35 +512,13 @@ func (u UIResults) GetUIResult(options ...ActionOption) (UIResult, error) {
return uiResults[idx], nil
}
func (dExt *DriverExt) GetUIResultMap(uiTypes []string) (uiResultMap UIResultMap, err error) {
var bufSource *bytes.Buffer
var imagePath string
if bufSource, imagePath, err = dExt.takeScreenShot(
builtin.GenNameWithTimestamp("%d_cv")); err != nil {
return
}
imageResult, err := dExt.ImageService.GetImage(bufSource, actionOptions{"ui"}, uiTypeOptions(uiTypes))
if err != nil {
log.Error().Err(err).Msg("GetImage from ImageService failed")
return
}
imageUrl := imageResult.URL
if imageUrl != "" {
dExt.cacheStepData.screenShotsUrls[imagePath] = imageUrl
log.Debug().Str("imagePath", imagePath).Str("imageUrl", imageUrl).Msg("log screenshot")
}
uiResultMap = imageResult.UIResult
return
}
func (dExt *DriverExt) FindUIResult(uiTypes []string, options ...ActionOption) (point PointF, err error) {
uiResultMap, err := dExt.GetUIResultMap(uiTypes)
screenResult, err := dExt.GetScreenResult(WithScreenShotUITypes(uiTypes...))
if err != nil {
return
}
uiResults, err := uiResultMap.FilterUIResults(uiTypes)
uiResults, err := screenResult.Icons.FilterUIResults(uiTypes)
if err != nil {
return
}

View File

@@ -76,7 +76,7 @@ func TestMatchRegex(t *testing.T) {
func TestTapUIWithScreenshot(t *testing.T) {
serialNumber := os.Getenv("SERIAL_NUMBER")
device, _ := NewAndroidDevice(WithSerialNumber(serialNumber))
driver, err := device.NewDriver(nil)
driver, err := device.NewDriver()
if err != nil {
t.Fatal(err)
}
@@ -88,7 +88,7 @@ func TestTapUIWithScreenshot(t *testing.T) {
}
func TestDriverExtOCR(t *testing.T) {
driverExt, err := iosDevice.NewDriver(nil)
driverExt, err := iosDevice.NewDriver()
checkErr(t, err)
point, err := driverExt.FindScreenText("抖音")

View File

@@ -143,7 +143,7 @@ func (dExt *DriverExt) swipeToTapTexts(texts []string, options ...ActionOption)
if err != nil {
log.Error().Err(err).Msg("swipeToTapTexts failed")
// target texts not found, try to auto handle popup
if e := dExt.AutoPopupHandler(screenTexts); e != nil {
if e := dExt.handleTextPopup(screenTexts); e != nil {
log.Error().Err(e).Msg("auto handle popup failed")
}
return err

View File

@@ -13,7 +13,7 @@ func init() {
}
func TestDriverExt_TapXY(t *testing.T) {
driverExt, err := iosDevice.NewDriver(nil)
driverExt, err := iosDevice.NewDriver()
checkErr(t, err)
err = driverExt.TapXY(0.4, 0.5)
@@ -21,7 +21,7 @@ func TestDriverExt_TapXY(t *testing.T) {
}
func TestDriverExt_TapAbsXY(t *testing.T) {
driverExt, err := iosDevice.NewDriver(nil)
driverExt, err := iosDevice.NewDriver()
checkErr(t, err)
err = driverExt.TapAbsXY(100, 300)
@@ -29,7 +29,7 @@ func TestDriverExt_TapAbsXY(t *testing.T) {
}
func TestDriverExt_TapWithOCR(t *testing.T) {
driverExt, err := iosDevice.NewDriver(nil)
driverExt, err := iosDevice.NewDriver()
checkErr(t, err)
// 需要点击文字上方的图标

View File

@@ -1,154 +1,15 @@
package uixt
import (
"strings"
"time"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
"github.com/httprunner/httprunner/v4/hrp/internal/code"
"github.com/httprunner/httprunner/v4/hrp/internal/json"
)
type VideoStat struct {
configs *VideoCrawlerConfigs
timer *time.Timer
FeedCount int `json:"feed_count"`
FeedStat map[string]int `json:"feed_stat"` // 分类统计 feed 数量:视频/图文/广告/特效/模板/购物
LiveCount int `json:"live_count"`
LiveStat map[string]int `json:"live_stat"` // 分类统计 live 数量:秀场/游戏/电商/多人
}
func (s *VideoStat) isFeedTargetAchieved() bool {
targetStat := make(map[string]int)
for _, targetLabel := range s.configs.Feed.TargetLabels {
targetStat[targetLabel.Text] = targetLabel.Target
}
log.Info().
Int("current_total", s.FeedCount).
Interface("current_stat", s.FeedStat).
Int("target_total", s.configs.Feed.TargetCount).
Interface("target_stat", targetStat).
Msg("display feed crawler progress")
// check total feed count
if s.FeedCount < s.configs.Feed.TargetCount {
return false
}
// check each feed type's count
for _, targetLabel := range s.configs.Feed.TargetLabels {
if s.FeedStat[targetLabel.Text] < targetLabel.Target {
return false
}
}
return true
}
func (s *VideoStat) isLiveTargetAchieved() bool {
targetStat := make(map[string]int)
for _, targetLabel := range s.configs.Live.TargetLabels {
targetStat[targetLabel.Text] = targetLabel.Target
}
log.Info().
Int("current_total", s.LiveCount).
Interface("current_stat", s.LiveStat).
Int("target_total", s.configs.Live.TargetCount).
Interface("target_stat", targetStat).
Msg("display live crawler progress")
// check total live count
if s.LiveCount < s.configs.Live.TargetCount {
return false
}
// check each live type's count
for _, targetLabel := range s.configs.Live.TargetLabels {
if s.LiveStat[targetLabel.Text] < targetLabel.Target {
return false
}
}
return true
}
func (s *VideoStat) isTargetAchieved() bool {
return s.isFeedTargetAchieved() && s.isLiveTargetAchieved()
}
// incrFeed increases feed count and feed stat
func (s *VideoStat) incrFeed(screenResult *ScreenResult, driverExt *DriverExt) error {
// feed author
actionOptions := []ActionOption{
WithRegex(true),
driverExt.GenAbsScope(0, 0.5, 1, 1).Option(),
}
if ocrText, err := screenResult.Texts.FindText("^@", actionOptions...); err == nil {
log.Debug().Str("author", ocrText.Text).Msg("found feed author")
screenResult.Tags = append(screenResult.Tags, ocrText.Text)
}
for _, targetLabel := range s.configs.Feed.TargetLabels {
scope := targetLabel.Scope
actionOptions := []ActionOption{
WithRegex(targetLabel.Regex),
driverExt.GenAbsScope(scope[0], scope[1], scope[2], scope[3]).Option(),
}
if _, err := screenResult.Texts.FindText(targetLabel.Text, actionOptions...); err == nil {
key := targetLabel.Text
if _, ok := s.FeedStat[key]; !ok {
s.FeedStat[key] = 0
}
s.FeedStat[key]++
screenResult.Tags = append(screenResult.Tags, key)
}
}
// add popularity data for feed
popularityData := screenResult.Texts.FilterScope(driverExt.GenAbsScope(0.8, 0.5, 1, 0.8))
if len(popularityData) != 4 {
log.Warn().Interface("popularity", popularityData).Msg("get feed popularity data failed")
} else {
screenResult.Popularity = Popularity{
Stars: popularityData[0].Text,
Comments: popularityData[1].Text,
Favorites: popularityData[2].Text,
Shares: popularityData[3].Text,
}
}
log.Info().Strs("tags", screenResult.Tags).
Interface("popularity", screenResult.Popularity).
Msg("found feed success")
s.FeedCount++
return nil
}
// incrLive increases live count and live stat
func (s *VideoStat) incrLive(screenResult *ScreenResult, driverExt *DriverExt) error {
// TODO: check live type
// add popularity data for live
popularityData := screenResult.Texts.FilterScope(driverExt.GenAbsScope(0.7, 0.05, 1, 0.15))
if len(popularityData) != 1 {
log.Warn().Interface("popularity", popularityData).Msg("get live popularity data failed")
} else {
screenResult.Popularity = Popularity{
LiveUsers: popularityData[0].Text,
}
}
log.Info().Strs("tags", screenResult.Tags).
Interface("popularity", screenResult.Popularity).
Msg("found live success")
s.LiveCount++
return nil
}
type TargetLabel struct {
Text string `json:"text"`
Scope Scope `json:"scope"`
@@ -169,20 +30,100 @@ type LiveConfig struct {
}
type VideoCrawlerConfigs struct {
AppPackageName string `json:"app_package_name"`
Timeout int `json:"timeout"` // seconds
Timeout int `json:"timeout"` // seconds
Feed FeedConfig `json:"feed"`
Live LiveConfig `json:"live"`
}
type LiveCrawler struct {
driver *DriverExt
configs *VideoCrawlerConfigs // target video count
currentStat *VideoStat // current video stat
type VideoCrawler struct {
driverExt *DriverExt
configs *VideoCrawlerConfigs
timer *time.Timer
// used to help checking if swipe success
failedCount int64
lastFeed *FeedVideo
lastLive *LiveRoom
FeedCount int `json:"feed_count"`
FeedStat map[string]int `json:"feed_stat"` // 分类统计 feed 数量:视频/图文/广告/特效/模板/购物
LiveCount int `json:"live_count"`
LiveStat map[string]int `json:"live_stat"` // 分类统计 live 数量:秀场/游戏/电商/多人
}
func (l *LiveCrawler) checkLiveVideo(texts OCRTexts) (enterPoint PointF, yes bool) {
func (vc *VideoCrawler) isFeedTargetAchieved() bool {
targetStat := make(map[string]int)
for _, targetLabel := range vc.configs.Feed.TargetLabels {
targetStat[targetLabel.Text] = targetLabel.Target
}
log.Info().
Int("current_total", vc.FeedCount).
Interface("current_stat", vc.FeedStat).
Int("target_total", vc.configs.Feed.TargetCount).
Interface("target_stat", targetStat).
Msg("display feed crawler progress")
// check total feed count
if vc.FeedCount < vc.configs.Feed.TargetCount {
return false
}
// check each feed type's count
for _, targetLabel := range vc.configs.Feed.TargetLabels {
if vc.FeedStat[targetLabel.Text] < targetLabel.Target {
return false
}
}
return true
}
func (vc *VideoCrawler) isLiveTargetAchieved() bool {
targetStat := make(map[string]int)
for _, targetLabel := range vc.configs.Live.TargetLabels {
targetStat[targetLabel.Text] = targetLabel.Target
}
log.Info().
Int("current_total", vc.LiveCount).
Interface("current_stat", vc.LiveStat).
Int("target_total", vc.configs.Live.TargetCount).
Interface("target_stat", targetStat).
Msg("display live crawler progress")
// check total live count
if vc.LiveCount < vc.configs.Live.TargetCount {
return false
}
// check each live type's count
for _, targetLabel := range vc.configs.Live.TargetLabels {
if vc.LiveStat[targetLabel.Text] < targetLabel.Target {
return false
}
}
return true
}
func (vc *VideoCrawler) isTargetAchieved() bool {
return vc.isFeedTargetAchieved() && vc.isLiveTargetAchieved()
}
func (vc *VideoCrawler) checkLiveVideo(feedVideo *FeedVideo) (enterPoint PointF, yes bool) {
// TODO: check if preview-live from feedVideo
if feedVideo.Type != "live" {
return PointF{}, false
}
// take screenshot and get OCR texts via image service
texts, err := vc.driverExt.GetScreenTexts()
if err != nil {
return PointF{}, false
}
// 预览流入口DY/KS
// 标签文案:点击进入直播间|进入直播间领金币
points, err := texts.FindTexts([]string{".*进入直播间.*"}, WithScope(0, 0.3, 1, 0.8), WithRegex(true))
@@ -214,93 +155,105 @@ func (l *LiveCrawler) checkLiveVideo(texts OCRTexts) (enterPoint PointF, yes boo
}
// run live video crawler
func (l *LiveCrawler) Run(driver *DriverExt, enterPoint PointF) error {
func (vc *VideoCrawler) startLiveCrawler(enterPoint PointF) error {
log.Info().Msg("enter live room")
if err := driver.TapAbsXY(enterPoint.X, enterPoint.Y); err != nil {
if err := vc.driverExt.TapAbsXY(enterPoint.X, enterPoint.Y); err != nil {
log.Error().Err(err).Msg("tap live video failed")
return err
}
time.Sleep(5 * time.Second)
for !l.currentStat.isLiveTargetAchieved() {
for !vc.isLiveTargetAchieved() {
select {
case <-l.currentStat.timer.C:
case <-vc.timer.C:
log.Warn().Msg("timeout in live crawler")
return errors.Wrap(code.TimeoutError, "live crawler timeout")
case <-l.driver.interruptSignal:
case <-vc.driverExt.interruptSignal:
log.Warn().Msg("interrupted in live crawler")
return errors.Wrap(code.InterruptError, "live crawler interrupted")
default:
// check if live room
if err := l.driver.Driver.AssertForegroundApp(l.configs.AppPackageName, "live"); err != nil {
// swipe to next live video
swipeStartTime := time.Now()
if err := vc.driverExt.SwipeUp(); err != nil {
log.Error().Err(err).Msg("live swipe up failed")
return err
}
swipeFinishTime := time.Now()
// swipe to next live video
err := l.driver.SwipeUp()
// wait for live video loading
time.Sleep(5 * time.Second)
// TODO: get app event trackings
liveRoom, err := vc.getCurrentLiveRoom()
if err != nil {
log.Error().Err(err).Msg("swipe up failed")
// TODO: retry maximum 3 times
continue
}
// sleep custom random time
if err := sleepRandom(time.Now(), l.configs.Live.SleepRandom); err != nil {
log.Error().Err(err).Msg("sleep random failed")
return errors.Wrap(err, "get current live event trackings failed")
}
// take screenshot and get screen texts by OCR
screenResult, err := l.driver.GetScreenResult()
screenResult, err := vc.driverExt.GetScreenResult(
WithScreenShotOCR(true), WithScreenShotUpload(true))
if err != nil {
log.Error().Err(err).Msg("OCR GetTexts failed")
time.Sleep(3 * time.Second)
continue
}
screenResult.Tags = append([]string{"live"}, screenResult.Tags...)
screenResult.Live = liveRoom
// check live type and incr live count
if err := l.currentStat.incrLive(screenResult, l.driver); err != nil {
log.Error().Err(err).Msg("incr live failed")
// TODO: check live type
// incr live count
screenResult.VideoType = "live"
vc.LiveCount++
log.Info().Strs("tags", screenResult.Tags).
Interface("live", screenResult.Live).
Msg("found live success")
// get simulation watch duration
if screenResult.Live.SimulationWatchDuration != 0 {
screenResult.Live.WatchDuration = screenResult.Live.SimulationWatchDuration
} else {
screenResult.Live.RandomWatchDuration = getSimulationDuration(vc.configs.Live.SleepRandom)
screenResult.Live.WatchDuration = screenResult.Live.RandomWatchDuration
}
// simulation watch live video
sleepStrict(swipeFinishTime, screenResult.Live.WatchDuration)
// log swipe timelines
screenResult.SwipeStartTime = swipeStartTime.UnixMilli()
screenResult.SwipeFinishTime = swipeFinishTime.UnixMilli()
screenResult.TotalElapsed = time.Since(swipeFinishTime).Milliseconds()
}
}
log.Info().Msg("live count achieved, exit live room")
return l.exitLiveRoom()
return vc.exitLiveRoom()
}
func (l *LiveCrawler) exitLiveRoom() error {
func (vc *VideoCrawler) exitLiveRoom() error {
for i := 0; i < 3; i++ {
l.driver.SwipeRelative(0.1, 0.5, 0.9, 0.5)
vc.driverExt.SwipeRelative(0.1, 0.5, 0.9, 0.5)
time.Sleep(2 * time.Second)
// check if back to feed page
if err := l.driver.Driver.AssertForegroundApp(l.configs.AppPackageName, "feed"); err == nil {
return nil
}
}
// exit live room failed, while video count achieved
if l.currentStat.isTargetAchieved() {
if vc.isTargetAchieved() {
return nil
}
// click X button on upper-right corner
if err := l.driver.TapXY(0.95, 0.05); err == nil {
if err := vc.driverExt.TapXY(0.95, 0.05); err == nil {
log.Info().Msg("tap X button on upper-right corner to exit live room")
time.Sleep(2 * time.Second)
// check if back to feed page
if err := l.driver.Driver.AssertForegroundApp(l.configs.AppPackageName, "feed"); err == nil {
return nil
}
}
return errors.New("exit live room failed")
}
func (dExt *DriverExt) VideoCrawler(configs *VideoCrawlerConfigs) (err error) {
if dExt.plugin == nil {
return errors.New("miss plugin for video crawler")
}
// set default sleep random strategy if not set
if configs.Feed.SleepRandom == nil {
configs.Feed.SleepRandom = []interface{}{1, 5}
@@ -309,8 +262,13 @@ func (dExt *DriverExt) VideoCrawler(configs *VideoCrawlerConfigs) (err error) {
configs.Live.SleepRandom = []interface{}{10, 15}
}
currVideoStat := &VideoStat{
configs: configs,
crawler := &VideoCrawler{
driverExt: dExt,
configs: configs,
failedCount: 0,
lastFeed: &FeedVideo{},
lastLive: &LiveRoom{},
FeedCount: 0,
FeedStat: make(map[string]int),
@@ -318,69 +276,65 @@ func (dExt *DriverExt) VideoCrawler(configs *VideoCrawlerConfigs) (err error) {
LiveStat: make(map[string]int),
}
defer func() {
dExt.cacheStepData.videoStat = currVideoStat
dExt.cacheStepData.videoCrawler = crawler
}()
// launch app
if configs.AppPackageName != "" {
if err = dExt.Driver.AppLaunch(configs.AppPackageName); err != nil {
return err
}
time.Sleep(5 * time.Second)
} else {
app, err := dExt.Driver.GetForegroundApp()
if err != nil && !errors.Is(err, errDriverNotImplemented) {
log.Warn().Err(err).Msg("get foreground app failed, ignore")
return errors.Wrap(code.MobileUIAssertForegroundAppError, err.Error())
}
log.Info().
Str("packageName", app.PackageName).
Str("activity", app.Activity).
Msg("start to video crawler for current foreground app")
configs.AppPackageName = app.PackageName
}
liveCrawler := LiveCrawler{
driver: dExt,
configs: configs,
currentStat: currVideoStat,
}
// loop until target count achieved or timeout
// the main loop is feed crawler
currVideoStat.timer = time.NewTimer(time.Duration(configs.Timeout) * time.Second)
lastSwipeTime := time.Now()
crawler.timer = time.NewTimer(time.Duration(configs.Timeout) * time.Second)
for {
select {
case <-currVideoStat.timer.C:
case <-crawler.timer.C:
log.Warn().Msg("timeout in feed crawler")
return errors.Wrap(code.TimeoutError, "feed crawler timeout")
case <-dExt.interruptSignal:
log.Warn().Msg("interrupted in feed crawler")
return errors.Wrap(code.InterruptError, "feed crawler interrupted")
default:
// take screenshot and get screen texts by OCR
screenResult, err := dExt.GetScreenResult()
if err != nil {
if strings.Contains(err.Error(), "connect: connection refused") {
return err
// swipe to next feed video
log.Info().Msg("swipe to next feed video")
swipeStartTime := time.Now()
if err = dExt.SwipeUp(); err != nil {
log.Error().Err(err).Msg("feed swipe up failed")
return err
}
swipeFinishTime := time.Now()
// get app event trackings
// retry 3 times if get feed failed, abort if fail 3 consecutive times
feedVideo, err := crawler.getCurrentFeedVideo()
if err != nil || feedVideo.VideoID == "" {
if crawler.failedCount >= 3 {
// failed 3 consecutive times
return errors.New("get current feed video failed 3 consecutive times")
}
log.Error().Err(err).Msg("OCR GetTexts failed")
time.Sleep(3 * time.Second)
log.Warn().Interface("feedVideo", feedVideo).Msg("get current feed video failed")
// retry
crawler.failedCount++
continue
}
// automatic handling of pop-up windows
if err := dExt.AutoPopupHandler(screenResult.Texts); err != nil {
log.Error().Err(err).Msg("auto handle popup failed")
return err
if feedVideo.VideoID == crawler.lastFeed.VideoID {
// app event tracking not changed
// check and handle popups
if err = crawler.driverExt.AutoPopupHandler(); err != nil {
return err
}
}
crawler.lastFeed = feedVideo
screenResult := &ScreenResult{}
dExt.cacheStepData.screenResults[time.Now().String()] = screenResult
// check if live video && run live crawler
if enterPoint, isLive := liveCrawler.checkLiveVideo(screenResult.Texts); isLive {
if enterPoint, isLive := crawler.checkLiveVideo(feedVideo); isLive {
// 直播预览流
screenResult.VideoType = "live-preview"
// TODO
// screenResult.Live = feedVideo
log.Info().Msg("live video found")
if !liveCrawler.currentStat.isLiveTargetAchieved() {
if err := liveCrawler.Run(dExt, enterPoint); err != nil {
if !crawler.isLiveTargetAchieved() {
if err := crawler.startLiveCrawler(enterPoint); err != nil {
if errors.Is(err, code.TimeoutError) || errors.Is(err, code.InterruptError) {
return err
}
@@ -388,39 +342,125 @@ func (dExt *DriverExt) VideoCrawler(configs *VideoCrawlerConfigs) (err error) {
continue
}
}
screenResult.Tags = []string{"live-preview"}
} else {
screenResult.Tags = []string{"feed"}
// 点播
// check feed type and incr feed count
if err := currVideoStat.incrFeed(screenResult, dExt); err != nil {
log.Error().Err(err).Msg("incr feed failed")
}
}
screenResult.VideoType = "feed"
screenResult.Feed = feedVideo
crawler.FeedCount++
log.Info().
Strs("tags", screenResult.Tags).
Interface("feed", screenResult.Feed).
Msg("found feed success")
// sleep custom random time
if err := sleepRandom(lastSwipeTime, configs.Feed.SleepRandom); err != nil {
log.Error().Err(err).Msg("sleep random failed")
// get simulation play duration
if screenResult.Feed.SimulationPlayDuration != 0 {
screenResult.Feed.PlayDuration = screenResult.Feed.SimulationPlayDuration
} else {
screenResult.Feed.RandomPlayDuration = getSimulationDuration(crawler.configs.Feed.SleepRandom)
screenResult.Feed.PlayDuration = screenResult.Feed.RandomPlayDuration
}
// simulation watch feed video
sleepStrict(swipeFinishTime, screenResult.Feed.PlayDuration)
}
// check if target count achieved
if currVideoStat.isTargetAchieved() {
if crawler.isTargetAchieved() {
log.Info().Msg("target count achieved, exit crawler")
return nil
}
// swipe to next feed video
log.Info().Msg("swipe to next feed video")
if err = dExt.SwipeUp(); err != nil {
log.Error().Err(err).Msg("swipe up failed")
return err
}
lastSwipeTime = time.Now()
// log swipe timelines
screenResult.SwipeStartTime = swipeStartTime.UnixMilli()
screenResult.SwipeFinishTime = swipeFinishTime.UnixMilli()
screenResult.TotalElapsed = time.Since(swipeFinishTime).Milliseconds()
// check if feed page
if err := dExt.Driver.AssertForegroundApp(configs.AppPackageName, "feed"); err != nil {
return err
}
// reset failed count
crawler.failedCount = 0
}
}
}
type FeedVideo struct {
// 视频基础数据
VideoID string `json:"video_id"` // 视频 video ID
UserName string `json:"user_name"` // 视频作者
Duration int64 `json:"duration"` // 视频时长(ms)
Caption string `json:"caption"` // 视频文案
Type string `json:"type"` // 视频类型, feed/live // TODO: 区分视频、图文、广告
// 视频热度数据
ViewCount int64 `json:"view_count"` // feed 观看数
LikeCount int64 `json:"like_count"` // feed 点赞数
CommentCount int64 `json:"comment_count"` // feed 评论数
CollectCount int64 `json:"collect_count"` // feed 收藏数
ForwardCount int64 `json:"forward_count"` // feed 转发数
ShareCount int64 `json:"share_count"` // feed 分享数
// 记录仿真决策信息
PlayDuration int64 `json:"play_duration"` // 播放时长(ms),取自 Simulation/Random
SimulationPlayProgress float64 `json:"simulation_play_progress"` // 仿真播放比例(完播率)
SimulationPlayDuration int64 `json:"simulation_play_duration"` // 仿真播放时长(ms)
RandomPlayDuration int64 `json:"random_play_duration"` // 随机播放时长(ms)
// timelines
PublishTimestamp int64 `json:"publish_timestamp"` // feed 发布时间戳
PreloadTimestamp int64 `json:"preload_timestamp"` // feed 预加载时间戳
}
type LiveRoom struct {
// 视频基础数据
LiveStreamID string `json:"live_stream_id"` // 直播流 ID
UserName string `json:"user_name"` // 视频作者
Caption string `json:"caption"` // 视频文案
LiveType string `json:"live_type"` // 直播间类型, 基于算法服务获取
// 视频热度数据
AudienceCount string `json:"audience_count"` // 直播间人数
LikeCount int64 `json:"like_count"` // 点赞数
// 记录仿真决策信息
WatchDuration int64 `json:"watch_duration"` // 观播时长(ms),取自 Simulation/Random
SimulationWatchDuration int64 `json:"simulation_watch_duration"` // 仿真观播时长(ms)
RandomWatchDuration int64 `json:"random_watch_duration"` // 随机观播时长(ms)
// timelines
PreloadTimestamp int64 `json:"preload_timestamp"` // feed 预加载时间戳
}
func (vc *VideoCrawler) getCurrentFeedVideo() (feedVideo *FeedVideo, err error) {
if !vc.driverExt.plugin.Has("GetCurrentFeedVideo") {
return nil, errors.New("plugin missing GetCurrentFeedVideo method")
}
resp, err := vc.driverExt.plugin.Call("GetCurrentFeedVideo")
if err != nil {
return nil, errors.Wrap(err, "call plugin GetCurrentFeedVideo failed")
}
if resp == nil {
return nil, errors.New("feed not found")
}
feedBytes, err := json.Marshal(resp)
if err != nil {
return nil, errors.New("json marshal feed video info failed")
}
feedVideo = &FeedVideo{}
err = json.Unmarshal(feedBytes, feedVideo)
if err != nil {
return nil, errors.Wrap(err, "json unmarshal feed video info failed")
}
log.Info().
Interface("feedVideoCaption", feedVideo.Caption).
Msg("get current feed video success")
return feedVideo, nil
}
func (vc *VideoCrawler) getCurrentLiveRoom() (liveVideo *LiveRoom, err error) {
// TODO
return
}

View File

@@ -2,15 +2,16 @@
package uixt
import "testing"
import (
"testing"
)
func TestVideoCrawler(t *testing.T) {
setupAndroid(t)
driverExt.Driver.AppLaunch("com.ss.android.ugc.aweme")
configs := &VideoCrawlerConfigs{
AppPackageName: "com.ss.android.ugc.aweme",
Timeout: 600,
Timeout: 600,
Feed: FeedConfig{
TargetCount: 5,
TargetLabels: []TargetLabel{

View File

@@ -1,20 +1,18 @@
package hrp
import (
"fmt"
"os"
"path/filepath"
"strings"
"sync"
"github.com/httprunner/funplugin"
"github.com/httprunner/funplugin/fungo"
"github.com/httprunner/funplugin/myexec"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
"github.com/httprunner/httprunner/v4/hrp/internal/code"
"github.com/httprunner/httprunner/v4/hrp/internal/env"
"github.com/httprunner/httprunner/v4/hrp/internal/myexec"
"github.com/httprunner/httprunner/v4/hrp/internal/sdk"
)
@@ -65,15 +63,13 @@ func initPlugin(path, venv string, logOn bool) (plugin funplugin.IPlugin, err er
}
pluginPath = genPyPluginPath
packages := []string{
fmt.Sprintf("funppy==%s", fungo.Version),
}
packages := []string{"funppy"}
python3, err := myexec.EnsurePython3Venv(venv, packages...)
if err != nil {
log.Error().Err(err).
Interface("packages", packages).
Msg("python3 venv is not ready")
return nil, err
return nil, errors.Wrap(code.InvalidPython3Venv, err.Error())
}
pluginOptions = append(pluginOptions, funplugin.WithPython3(python3))
}
@@ -120,14 +116,15 @@ func locatePlugin(path string) (pluginPath string, err error) {
return
}
return "", fmt.Errorf("plugin file not found")
return "", errors.New("plugin file not found")
}
// locateFile searches destFile upward recursively until system root dir
func locateFile(startPath string, destFile string) (string, error) {
// if not found, then searches in hrp executable dir
func locateFile(startPath string, destFile string) (pluginPath string, err error) {
stat, err := os.Stat(startPath)
if os.IsNotExist(err) {
return "", err
return "", errors.Wrap(err, "start path not exists")
}
var startDir string
@@ -139,7 +136,7 @@ func locateFile(startPath string, destFile string) (string, error) {
startDir, _ = filepath.Abs(startDir)
// convention over configuration
pluginPath := filepath.Join(startDir, destFile)
pluginPath = filepath.Join(startDir, destFile)
if _, err := os.Stat(pluginPath); err == nil {
return pluginPath, nil
}
@@ -147,12 +144,31 @@ func locateFile(startPath string, destFile string) (string, error) {
// system root dir
parentDir, _ := filepath.Abs(filepath.Dir(startDir))
if parentDir == startDir {
return "", fmt.Errorf("searched to system root dir, plugin file not found")
if pluginPath, err = locateExecutable(destFile); err == nil {
return
}
return "", errors.New("searched to system root dir, plugin file not found")
}
return locateFile(parentDir, destFile)
}
// locateExecutable finds destFile in hrp executable dir
func locateExecutable(destFile string) (string, error) {
exePath, err := os.Executable()
if err != nil {
return "", errors.Wrap(err, "get hrp executable failed")
}
exeDir := filepath.Dir(exePath)
pluginPath := filepath.Join(exeDir, destFile)
if _, err := os.Stat(pluginPath); err == nil {
return pluginPath, nil
}
return "", errors.New("plugin file not found in hrp executable dir")
}
func GetProjectRootDirPath(path string) (rootDir string, err error) {
pluginPath, err := locatePlugin(path)
if err == nil {

View File

@@ -84,9 +84,8 @@ type HRPRunner struct {
httpClient *http.Client
http2Client *http.Client
wsDialer *websocket.Dialer
uiClients map[string]*uixt.DriverExt // UI automation clients for iOS and Android, key is udid/serial
caseTimeoutTimer *time.Timer // case timeout timer
interruptSignal chan os.Signal // interrupt signal channel
caseTimeoutTimer *time.Timer // case timeout timer
interruptSignal chan os.Signal // interrupt signal channel
}
// SetClientTransport configures transport of http client for high concurrency load testing
@@ -236,7 +235,7 @@ func (r *HRPRunner) Run(testcases ...ITestCase) (err error) {
// release UI driver session
defer func() {
for _, client := range r.uiClients {
for _, client := range caseRunner.uiClients {
client.Driver.DeleteSession()
}
}()
@@ -339,13 +338,15 @@ func (r *HRPRunner) NewCaseRunner(testcase *TestCase) (*CaseRunner, error) {
}
type CaseRunner struct {
testCase *TestCase
hrpRunner *HRPRunner
parser *Parser
hrpRunner *HRPRunner // all case runners share one HRPRunner
testCase *TestCase // each testcase init its own CaseRunner
parser *Parser // each CaseRunner init its own Parser
parsedConfig *TConfig
parametersIterator *ParametersIterator
rootDir string // project root dir
rootDir string // project root dir
uiClients map[string]*uixt.DriverExt // UI automation clients for iOS and Android, key is udid/serial
}
// parseConfig parses testcase config, stores to parsedConfig.
@@ -418,8 +419,8 @@ func (r *CaseRunner) parseConfig() error {
r.parametersIterator = parametersIterator
// init iOS/Android clients
if r.hrpRunner.uiClients == nil {
r.hrpRunner.uiClients = make(map[string]*uixt.DriverExt)
if r.uiClients == nil {
r.uiClients = make(map[string]*uixt.DriverExt)
}
for _, iosDeviceConfig := range r.parsedConfig.IOS {
if iosDeviceConfig.UDID != "" {
@@ -434,11 +435,11 @@ func (r *CaseRunner) parseConfig() error {
if err != nil {
return errors.Wrap(err, "init iOS device failed")
}
client, err := device.NewDriver(nil)
client, err := device.NewDriver(uixt.WithDriverPlugin(r.parser.plugin))
if err != nil {
return errors.Wrap(err, "init iOS WDA client failed")
}
r.hrpRunner.uiClients[device.UDID] = client
r.uiClients[device.UDID] = client
}
for _, androidDeviceConfig := range r.parsedConfig.Android {
if androidDeviceConfig.SerialNumber != "" {
@@ -453,11 +454,11 @@ func (r *CaseRunner) parseConfig() error {
if err != nil {
return errors.Wrap(err, "init Android device failed")
}
client, err := device.NewDriver(nil)
client, err := device.NewDriver(uixt.WithDriverPlugin(r.parser.plugin))
if err != nil {
return errors.Wrap(err, "init Android client failed")
}
r.hrpRunner.uiClients[device.SerialNumber] = client
r.uiClients[device.SerialNumber] = client
}
return nil
@@ -476,7 +477,8 @@ func (r *CaseRunner) NewSession() *SessionRunner {
// SessionRunner is used to run testcase and its steps.
// each testcase has its own SessionRunner instance and share session variables.
type SessionRunner struct {
caseRunner *CaseRunner
caseRunner *CaseRunner // all session runners share one CaseRunner
sessionVariables map[string]interface{}
// transactions stores transaction timing info.
// key is transaction name, value is map of transaction type and time, e.g. start time and end time.
@@ -584,12 +586,12 @@ func (r *SessionRunner) Start(givenVars map[string]interface{}) error {
r.sessionVariables[k] = v
}
stepElapsed := time.Since(stepStartTime).Seconds()
stepElapsed := time.Since(stepStartTime).Milliseconds()
if err == nil {
log.Info().Str("step", stepName).
Str("type", stepType).
Bool("success", true).
Float64("elapsed(s)", stepElapsed).
Int64("elapsed(ms)", stepElapsed).
Interface("exportVars", stepResult.ExportVars).
Msg("run step end")
continue
@@ -599,7 +601,7 @@ func (r *SessionRunner) Start(givenVars map[string]interface{}) error {
log.Error().Err(err).Str("step", stepName).
Str("type", stepType).
Bool("success", false).
Float64("elapsed(s)", stepElapsed).
Int64("elapsed(ms)", stepElapsed).
Msg("run step end")
// interrupted or timeout, abort running
@@ -661,7 +663,7 @@ func (r *SessionRunner) GetSummary() (*TestCaseSummary, error) {
caseSummary.InOut.ExportVars = exportVars
caseSummary.InOut.ConfigVars = r.caseRunner.parsedConfig.Variables
for uuid, client := range r.caseRunner.hrpRunner.uiClients {
for uuid, client := range r.caseRunner.uiClients {
// add WDA/UIA logs to summary
logs := map[string]interface{}{
"uuid": uuid,

View File

@@ -289,11 +289,11 @@ func (s *StepMobile) VideoCrawler(params map[string]interface{}) *StepMobile {
return &StepMobile{step: s.step}
}
func (s *StepMobile) ScreenShot() *StepMobile {
func (s *StepMobile) ScreenShot(options ...uixt.ActionOption) *StepMobile {
s.mobileStep().Actions = append(s.mobileStep().Actions, uixt.MobileAction{
Method: uixt.ACTION_ScreenShot,
Params: nil,
Options: nil,
Options: uixt.NewActionOptions(options...),
})
return &StepMobile{step: s.step}
}
@@ -513,7 +513,7 @@ func (s *StepMobileUIValidation) Run(r *SessionRunner) (*StepResult, error) {
return runStepMobileUI(r, s.step)
}
func (r *HRPRunner) initUIClient(uuid string, osType string) (client *uixt.DriverExt, err error) {
func (r *CaseRunner) initUIClient(uuid string, osType string) (client *uixt.DriverExt, err error) {
// avoid duplicate init
if uuid == "" && len(r.uiClients) > 0 {
for _, v := range r.uiClients {
@@ -538,7 +538,7 @@ func (r *HRPRunner) initUIClient(uuid string, osType string) (client *uixt.Drive
return nil, errors.Wrapf(err, "init %s device failed", osType)
}
client, err = device.NewDriver(nil)
client, err = device.NewDriver(uixt.WithDriverPlugin(r.parser.plugin))
if err != nil {
return nil, err
}
@@ -585,7 +585,7 @@ func runStepMobileUI(s *SessionRunner, step *TStep) (stepResult *StepResult, err
}
// init wda/uia driver
uiDriver, err := s.caseRunner.hrpRunner.initUIClient(mobileStep.Serial, osType)
uiDriver, err := s.caseRunner.initUIClient(mobileStep.Serial, osType)
if err != nil {
return
}
@@ -603,13 +603,9 @@ func runStepMobileUI(s *SessionRunner, step *TStep) (stepResult *StepResult, err
}
}
// take screenshot and get screen texts by OCR
screenTexts, err2 := uiDriver.GetScreenTexts()
if err2 != nil {
log.Error().Err(err2).Str("step", step.Name).Msg("take screenshot failed on step finished")
} else if err3 := uiDriver.AutoPopupHandler(screenTexts); err3 != nil {
// automatic handling of pop-up windows on each step finished
log.Error().Err(err3).Msg("auto handle popup failed")
// automatic handling of pop-up windows on each step finished
if err2 := uiDriver.AutoPopupHandler(); err2 != nil {
log.Error().Err(err2).Str("step", step.Name).Msg("auto handle popup failed")
}
// save attachments