mirror of
https://github.com/httprunner/httprunner.git
synced 2026-05-31 05:19:38 +08:00
feat: 支持shoots协议,新增UIAgent驱动接口
This commit is contained in:
25
hrp/cmd/server.go
Normal file
25
hrp/cmd/server.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/httprunner/httprunner/v4/hrp/pkg/server"
|
||||
)
|
||||
|
||||
// serverCmd represents the server command
|
||||
var serverCmd = &cobra.Command{
|
||||
Use: "server start",
|
||||
Short: "start hrp server",
|
||||
Long: `start hrp server. exec automation by http`,
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return server.NewServer(port)
|
||||
},
|
||||
}
|
||||
|
||||
var port int
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(serverCmd)
|
||||
serverCmd.Flags().IntVarP(&port, "port", "p", 8082, "Port to run the server on")
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
v4.5.8
|
||||
v4.6.0
|
||||
|
||||
@@ -292,6 +292,33 @@ func (d *Device) ReverseForwardKill(remoteInterface interface{}) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *Device) RunShootsCommand(command []byte, processName string) (res string, err error) {
|
||||
var tp transport
|
||||
if tp, err = d.createDeviceTransport(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer func() { _ = tp.Close() }()
|
||||
|
||||
if err = tp.SendWithCheck(fmt.Sprintf("localabstract:%s", processName)); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err = tp.SendBytes(command); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
lenBuf, err := tp.ReadBytesN(4)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
length := binary.LittleEndian.Uint32(lenBuf)
|
||||
result, err := tp.ReadBytesN(int(length) - 4)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(result), nil
|
||||
}
|
||||
|
||||
func (d *Device) ReverseForwardKillAll() error {
|
||||
_, err := d.executeCommand("reverse:killforward-all")
|
||||
return err
|
||||
|
||||
18
hrp/pkg/server/exception.go
Normal file
18
hrp/pkg/server/exception.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package server
|
||||
|
||||
// 常见的错误代码和消息
|
||||
const (
|
||||
InternalServerErrorCode = 100001
|
||||
InternalServerErrorMsg = "Invalid Server Error"
|
||||
|
||||
InvalidParamErrorCode = 100002
|
||||
InvalidParamErrorMsg = "Invalid %s Param"
|
||||
|
||||
CodeNotFound = 1004
|
||||
MsgNotFound = "Resource not found"
|
||||
)
|
||||
|
||||
const (
|
||||
DeviceNotFoundCode = 110001
|
||||
DeviceNotFoundMsg = "Device %s Not Found"
|
||||
)
|
||||
44
hrp/pkg/server/model.go
Normal file
44
hrp/pkg/server/model.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package server
|
||||
|
||||
type HttpResponse struct {
|
||||
Result interface{} `json:"result,omitempty"`
|
||||
ErrorCode int `json:"errorCode"`
|
||||
ErrorMsg string `json:"errorMsg"`
|
||||
}
|
||||
|
||||
type TapRequest struct {
|
||||
X float64 `json:"x"`
|
||||
Y float64 `json:"y"`
|
||||
Duration float64 `json:"duration"`
|
||||
}
|
||||
|
||||
type DragRequest struct {
|
||||
FromX float64 `json:"from_x"`
|
||||
FromY float64 `json:"from_y"`
|
||||
ToX float64 `json:"to_x"`
|
||||
ToY float64 `json:"to_y"`
|
||||
Duration float64 `json:"duration"`
|
||||
}
|
||||
|
||||
type InputRequest struct {
|
||||
Text string `json:"text"`
|
||||
Frequency int `json:"frequency"` // only iOS
|
||||
}
|
||||
|
||||
type KeycodeRequest struct {
|
||||
Keycode int `json:"keycode"`
|
||||
}
|
||||
|
||||
type AppLaunchRequest struct {
|
||||
PackageName string `json:"packageName"`
|
||||
}
|
||||
|
||||
type AppTerminalRequest struct {
|
||||
PackageName string `json:"packageName"`
|
||||
}
|
||||
|
||||
type LoginRequest struct {
|
||||
PackageName string `json:"packageName"`
|
||||
PhoneNumber string `json:"phoneNumber"`
|
||||
Captcha string `json:"captcha"`
|
||||
}
|
||||
514
hrp/pkg/server/server.go
Normal file
514
hrp/pkg/server/server.go
Normal file
@@ -0,0 +1,514 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/httprunner/httprunner/v4/hrp/pkg/gadb"
|
||||
"github.com/httprunner/httprunner/v4/hrp/pkg/uixt"
|
||||
)
|
||||
|
||||
func NewServer(port int) error {
|
||||
router := gin.Default()
|
||||
router.GET("/ping", pingHandler)
|
||||
router.GET("/api/v1/:platform/devices", listDeviceHandler)
|
||||
router.POST("/api/v1/:platform/:serial/ui/tap", parseDeviceInfo(), tapHandler)
|
||||
router.POST("/api/v1/:platform/:serial/ui/drag", parseDeviceInfo(), dragHandler)
|
||||
router.POST("/api/v1/:platform/:serial/ui/input", parseDeviceInfo(), inputHandler)
|
||||
router.POST("/api/v1/:platform/:serial/key/unlock", parseDeviceInfo(), unlockHandler)
|
||||
router.POST("/api/v1/:platform/:serial/key/home", parseDeviceInfo(), homeHandler)
|
||||
router.POST("/api/v1/:platform/:serial/key", parseDeviceInfo(), keycodeHandler)
|
||||
router.GET("/api/v1/:platform/:serial/app/foreground", parseDeviceInfo(), foregroundAppHandler)
|
||||
router.POST("/api/v1/:platform/:serial/app/launch", parseDeviceInfo(), launchAppHandler)
|
||||
router.POST("/api/v1/:platform/:serial/app/terminal", parseDeviceInfo(), terminalAppHandler)
|
||||
router.GET("/api/v1/:platform/:serial/screenshot", parseDeviceInfo(), screenshotHandler)
|
||||
router.GET("/api/v1/:platform/:serial/shoots/source", parseDeviceInfo(), sourceHandler)
|
||||
router.GET("/api/v1/:platform/:serial/adb/source", parseDeviceInfo(), adbSourceHandler)
|
||||
router.POST("/api/v1/:platform/:serial/shoots/login", parseDeviceInfo(), loginHandler)
|
||||
|
||||
err := router.Run(fmt.Sprintf("127.0.0.1:%d", port))
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to start http server")
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func pingHandler(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, HttpResponse{Result: true})
|
||||
return
|
||||
}
|
||||
|
||||
func listDeviceHandler(c *gin.Context) {
|
||||
platform := c.Param("platform")
|
||||
switch strings.ToLower(platform) {
|
||||
case "android":
|
||||
{
|
||||
client, err := gadb.NewClient()
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to init adb client")
|
||||
c.JSON(http.StatusInternalServerError, HttpResponse{Result: false, ErrorCode: InternalServerErrorCode, ErrorMsg: InternalServerErrorMsg})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
devices, err := client.DeviceList()
|
||||
if err != nil && strings.Contains(err.Error(), "no android device found") {
|
||||
c.JSON(http.StatusOK, HttpResponse{Result: nil})
|
||||
return
|
||||
} else if err != nil {
|
||||
log.Err(err).Msg("failed to list devices")
|
||||
c.JSON(http.StatusInternalServerError, HttpResponse{Result: false, ErrorCode: InternalServerErrorCode, ErrorMsg: InternalServerErrorMsg})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
var deviceList []interface{}
|
||||
for _, device := range devices {
|
||||
brand, err := device.Brand()
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to get device brand")
|
||||
c.JSON(http.StatusInternalServerError, HttpResponse{Result: false, ErrorCode: InternalServerErrorCode, ErrorMsg: InternalServerErrorMsg})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
model, err := device.Model()
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to get device model")
|
||||
c.JSON(http.StatusInternalServerError, HttpResponse{Result: false, ErrorCode: InternalServerErrorCode, ErrorMsg: InternalServerErrorMsg})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
deviceInfo := map[string]interface{}{
|
||||
"serial": device.Serial(),
|
||||
"brand": brand,
|
||||
"model": model,
|
||||
"platform": "android",
|
||||
}
|
||||
deviceList = append(deviceList, deviceInfo)
|
||||
}
|
||||
c.JSON(http.StatusOK, HttpResponse{Result: deviceList})
|
||||
return
|
||||
}
|
||||
default:
|
||||
{
|
||||
c.JSON(http.StatusBadRequest, HttpResponse{
|
||||
ErrorCode: InvalidParamErrorCode,
|
||||
ErrorMsg: fmt.Sprintf(InvalidParamErrorMsg, "platform"),
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func tapHandler(c *gin.Context) {
|
||||
var tapReq TapRequest
|
||||
if err := c.ShouldBindJSON(&tapReq); err != nil {
|
||||
log.Err(err).Msg(fmt.Sprintf("[%s]: Invalid Request", c.HandlerName()))
|
||||
c.JSON(http.StatusBadRequest, HttpResponse{Result: false, ErrorCode: InvalidParamErrorCode, ErrorMsg: fmt.Sprintf(InvalidParamErrorMsg, "request")})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
driverObj, exists := c.Get("driver")
|
||||
if !exists {
|
||||
log.Error().Msg(fmt.Sprintf("[%s]: Driver Not exsit", c.HandlerName()))
|
||||
c.JSON(http.StatusBadRequest, HttpResponse{Result: false, ErrorCode: InvalidParamErrorCode, ErrorMsg: fmt.Sprintf(InvalidParamErrorMsg, "driver")})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
dExt := driverObj.(*uixt.DriverExt)
|
||||
if tapReq.X < 1 && tapReq.Y < 1 {
|
||||
err := dExt.TapXY(tapReq.X, tapReq.Y, uixt.WithPressDuration(tapReq.Duration))
|
||||
if err != nil {
|
||||
log.Err(err).Msg(fmt.Sprintf("[%s]: failed to tap %f, %f", c.HandlerName(), tapReq.X, tapReq.Y))
|
||||
c.JSON(http.StatusInternalServerError, HttpResponse{Result: false, ErrorCode: InternalServerErrorCode, ErrorMsg: InternalServerErrorMsg})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
} else {
|
||||
err := dExt.TapAbsXY(tapReq.X, tapReq.Y, uixt.WithPressDuration(tapReq.Duration))
|
||||
if err != nil {
|
||||
log.Err(err).Msg(fmt.Sprintf("[%s]: failed to tap %f, %f", c.HandlerName(), tapReq.X, tapReq.Y))
|
||||
c.JSON(http.StatusInternalServerError, HttpResponse{Result: false, ErrorCode: InternalServerErrorCode, ErrorMsg: InternalServerErrorMsg})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
}
|
||||
c.JSON(http.StatusOK, HttpResponse{Result: true})
|
||||
return
|
||||
}
|
||||
|
||||
func dragHandler(c *gin.Context) {
|
||||
var dragReq DragRequest
|
||||
if err := c.ShouldBindJSON(&dragReq); err != nil {
|
||||
log.Err(err).Msg(fmt.Sprintf("[%s]: Invalid Request", c.HandlerName()))
|
||||
c.JSON(http.StatusBadRequest, HttpResponse{Result: false, ErrorCode: InvalidParamErrorCode, ErrorMsg: fmt.Sprintf(InvalidParamErrorMsg, "request")})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
driverObj, exists := c.Get("driver")
|
||||
if !exists {
|
||||
log.Error().Msg(fmt.Sprintf("[%s]: Driver Not exsit", c.HandlerName()))
|
||||
c.JSON(http.StatusBadRequest, HttpResponse{Result: false, ErrorCode: InvalidParamErrorCode, ErrorMsg: fmt.Sprintf(InvalidParamErrorMsg, "driver")})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
dExt := driverObj.(*uixt.DriverExt)
|
||||
if dragReq.FromX < 1 && dragReq.FromY < 1 && dragReq.ToX < 1 && dragReq.ToY < 1 {
|
||||
err := dExt.SwipeRelative(dragReq.FromX, dragReq.FromY, dragReq.ToX, dragReq.ToY, uixt.WithPressDuration(dragReq.Duration))
|
||||
if err != nil {
|
||||
log.Err(err).Msg(fmt.Sprintf("[%s]: failed to drag from %f, %f to %f, %f", c.HandlerName(), dragReq.FromX, dragReq.FromY, dragReq.ToX, dragReq.ToY))
|
||||
c.JSON(http.StatusInternalServerError, HttpResponse{Result: false, ErrorCode: InternalServerErrorCode, ErrorMsg: InternalServerErrorMsg})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
} else {
|
||||
err := dExt.Driver.SwipeFloat(dragReq.FromX, dragReq.FromY, dragReq.ToX, dragReq.ToY, uixt.WithPressDuration(dragReq.Duration))
|
||||
if err != nil {
|
||||
log.Err(err).Msg(fmt.Sprintf("[%s]: failed to drag from %f, %f to %f, %f", c.HandlerName(), dragReq.FromX, dragReq.FromY, dragReq.ToX, dragReq.ToY))
|
||||
c.JSON(http.StatusInternalServerError, HttpResponse{Result: false, ErrorCode: InternalServerErrorCode, ErrorMsg: InternalServerErrorMsg})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
}
|
||||
c.JSON(http.StatusOK, HttpResponse{Result: true})
|
||||
return
|
||||
}
|
||||
|
||||
func inputHandler(c *gin.Context) {
|
||||
var inputReq InputRequest
|
||||
if err := c.ShouldBindJSON(&inputReq); err != nil {
|
||||
log.Err(err).Msg(fmt.Sprintf("[%s]: Invalid Request", c.HandlerName()))
|
||||
c.JSON(http.StatusBadRequest, HttpResponse{Result: false, ErrorCode: InvalidParamErrorCode, ErrorMsg: fmt.Sprintf(InvalidParamErrorMsg, "request")})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
driverObj, exists := c.Get("driver")
|
||||
if !exists {
|
||||
log.Error().Msg(fmt.Sprintf("[%s]: Driver Not exsit", c.HandlerName()))
|
||||
c.JSON(http.StatusBadRequest, HttpResponse{Result: false, ErrorCode: InvalidParamErrorCode, ErrorMsg: fmt.Sprintf(InvalidParamErrorMsg, "driver")})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
dExt := driverObj.(*uixt.DriverExt)
|
||||
err := dExt.Driver.SendKeys(inputReq.Text, uixt.WithFrequency(inputReq.Frequency))
|
||||
if err != nil {
|
||||
log.Err(err).Msg(fmt.Sprintf("[%s]: failed to input text %s", c.HandlerName(), inputReq.Text))
|
||||
c.JSON(http.StatusInternalServerError, HttpResponse{Result: false, ErrorCode: InternalServerErrorCode, ErrorMsg: InternalServerErrorMsg})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, HttpResponse{Result: true})
|
||||
return
|
||||
}
|
||||
|
||||
func unlockHandler(c *gin.Context) {
|
||||
driverObj, exists := c.Get("driver")
|
||||
if !exists {
|
||||
log.Error().Msg(fmt.Sprintf("[%s]: Driver Not exsit", c.HandlerName()))
|
||||
c.JSON(http.StatusBadRequest, HttpResponse{Result: false, ErrorCode: InvalidParamErrorCode, ErrorMsg: fmt.Sprintf(InvalidParamErrorMsg, "driver")})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
dExt := driverObj.(*uixt.DriverExt)
|
||||
err := dExt.Driver.Unlock()
|
||||
if err != nil {
|
||||
log.Err(err).Msg(fmt.Sprintf("[%s]: failed to unlick screen", c.HandlerName()))
|
||||
c.JSON(http.StatusInternalServerError, HttpResponse{Result: false, ErrorCode: InternalServerErrorCode, ErrorMsg: InternalServerErrorMsg})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, HttpResponse{Result: true})
|
||||
return
|
||||
}
|
||||
|
||||
func homeHandler(c *gin.Context) {
|
||||
driverObj, exists := c.Get("driver")
|
||||
if !exists {
|
||||
log.Error().Msg(fmt.Sprintf("[%s]: Driver Not exsit", c.HandlerName()))
|
||||
c.JSON(http.StatusBadRequest, HttpResponse{Result: false, ErrorCode: InvalidParamErrorCode, ErrorMsg: fmt.Sprintf(InvalidParamErrorMsg, "driver")})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
dExt := driverObj.(*uixt.DriverExt)
|
||||
err := dExt.Driver.Homescreen()
|
||||
if err != nil {
|
||||
log.Err(err).Msg(fmt.Sprintf("[%s]: failed to enter homescreen", c.HandlerName()))
|
||||
c.JSON(http.StatusInternalServerError, HttpResponse{Result: false, ErrorCode: InternalServerErrorCode, ErrorMsg: InternalServerErrorMsg})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, HttpResponse{Result: true})
|
||||
return
|
||||
}
|
||||
|
||||
func keycodeHandler(c *gin.Context) {
|
||||
var keycodeReq KeycodeRequest
|
||||
if err := c.ShouldBindJSON(&keycodeReq); err != nil {
|
||||
log.Err(err).Msg(fmt.Sprintf("[%s]: Invalid Request", c.HandlerName()))
|
||||
c.JSON(http.StatusBadRequest, HttpResponse{Result: false, ErrorCode: InvalidParamErrorCode, ErrorMsg: fmt.Sprintf(InvalidParamErrorMsg, "request")})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
driverObj, exists := c.Get("driver")
|
||||
if !exists {
|
||||
log.Error().Msg(fmt.Sprintf("[%s]: Driver Not exsit", c.HandlerName()))
|
||||
c.JSON(http.StatusBadRequest, HttpResponse{Result: false, ErrorCode: InvalidParamErrorCode, ErrorMsg: fmt.Sprintf(InvalidParamErrorMsg, "driver")})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
dExt := driverObj.(*uixt.DriverExt)
|
||||
err := dExt.Driver.PressKeyCode(uixt.KeyCode(keycodeReq.Keycode))
|
||||
if err != nil {
|
||||
log.Err(err).Msg(fmt.Sprintf("[%s]: failed to input keycode %d", c.HandlerName(), keycodeReq.Keycode))
|
||||
c.JSON(http.StatusInternalServerError, HttpResponse{Result: false, ErrorCode: InternalServerErrorCode, ErrorMsg: InternalServerErrorMsg})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, HttpResponse{Result: true})
|
||||
return
|
||||
}
|
||||
|
||||
func foregroundAppHandler(c *gin.Context) {
|
||||
driverObj, exists := c.Get("driver")
|
||||
if !exists {
|
||||
log.Error().Msg(fmt.Sprintf("[%s]: Driver Not exsit", c.HandlerName()))
|
||||
c.JSON(http.StatusBadRequest, HttpResponse{Result: false, ErrorCode: InvalidParamErrorCode, ErrorMsg: fmt.Sprintf(InvalidParamErrorMsg, "driver")})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
dExt := driverObj.(*uixt.DriverExt)
|
||||
appInfo, err := dExt.Driver.GetForegroundApp()
|
||||
if err != nil {
|
||||
log.Err(err).Msg(fmt.Sprintf("[%s]: failed to unlick screen", c.HandlerName()))
|
||||
c.JSON(http.StatusInternalServerError, HttpResponse{Result: false, ErrorCode: InternalServerErrorCode, ErrorMsg: InternalServerErrorMsg})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, HttpResponse{Result: appInfo})
|
||||
return
|
||||
}
|
||||
|
||||
func launchAppHandler(c *gin.Context) {
|
||||
var appLaunchReq AppLaunchRequest
|
||||
if err := c.ShouldBindJSON(&appLaunchReq); err != nil {
|
||||
log.Err(err).Msg(fmt.Sprintf("[%s]: Invalid Request", c.HandlerName()))
|
||||
c.JSON(http.StatusBadRequest, HttpResponse{Result: "", ErrorCode: InvalidParamErrorCode, ErrorMsg: fmt.Sprintf(InvalidParamErrorMsg, "request")})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
driverObj, exists := c.Get("driver")
|
||||
if !exists {
|
||||
log.Error().Msg(fmt.Sprintf("[%s]: Driver Not exsit", c.HandlerName()))
|
||||
c.JSON(http.StatusBadRequest, HttpResponse{Result: "", ErrorCode: InvalidParamErrorCode, ErrorMsg: fmt.Sprintf(InvalidParamErrorMsg, "driver")})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
dExt := driverObj.(*uixt.DriverExt)
|
||||
err := dExt.Driver.AppLaunch(appLaunchReq.PackageName)
|
||||
if err != nil {
|
||||
log.Err(err).Msg(fmt.Sprintf("[%s]: failed to launch app %s", c.HandlerName(), appLaunchReq.PackageName))
|
||||
c.JSON(http.StatusInternalServerError, HttpResponse{Result: "", ErrorCode: InternalServerErrorCode, ErrorMsg: InternalServerErrorMsg})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, HttpResponse{Result: true})
|
||||
return
|
||||
}
|
||||
|
||||
func terminalAppHandler(c *gin.Context) {
|
||||
var appTerminalReq AppTerminalRequest
|
||||
if err := c.ShouldBindJSON(&appTerminalReq); err != nil {
|
||||
log.Err(err).Msg(fmt.Sprintf("[%s]: Invalid Request", c.HandlerName()))
|
||||
c.JSON(http.StatusBadRequest, HttpResponse{Result: "", ErrorCode: InvalidParamErrorCode, ErrorMsg: fmt.Sprintf(InvalidParamErrorMsg, "request")})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
driverObj, exists := c.Get("driver")
|
||||
if !exists {
|
||||
log.Error().Msg(fmt.Sprintf("[%s]: Driver Not exsit", c.HandlerName()))
|
||||
c.JSON(http.StatusBadRequest, HttpResponse{Result: "", ErrorCode: InvalidParamErrorCode, ErrorMsg: fmt.Sprintf(InvalidParamErrorMsg, "driver")})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
dExt := driverObj.(*uixt.DriverExt)
|
||||
success, err := dExt.Driver.AppTerminate(appTerminalReq.PackageName)
|
||||
if !success {
|
||||
log.Err(err).Msg(fmt.Sprintf("[%s]: failed to launch app %s", c.HandlerName(), appTerminalReq.PackageName))
|
||||
c.JSON(http.StatusInternalServerError, HttpResponse{Result: "", ErrorCode: InternalServerErrorCode, ErrorMsg: InternalServerErrorMsg})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, HttpResponse{Result: true})
|
||||
return
|
||||
}
|
||||
|
||||
func screenshotHandler(c *gin.Context) {
|
||||
driverObj, exists := c.Get("driver")
|
||||
if !exists {
|
||||
log.Error().Msg(fmt.Sprintf("[%s]: Driver Not exsit", c.HandlerName()))
|
||||
c.JSON(http.StatusBadRequest, HttpResponse{Result: "", ErrorCode: InvalidParamErrorCode, ErrorMsg: fmt.Sprintf(InvalidParamErrorMsg, "driver")})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
dExt := driverObj.(*uixt.DriverExt)
|
||||
raw, err := dExt.Driver.Screenshot()
|
||||
if err != nil {
|
||||
log.Err(err).Msg(fmt.Sprintf("[%s]: failed to get screenshot", c.HandlerName()))
|
||||
c.JSON(http.StatusInternalServerError, HttpResponse{Result: "", ErrorCode: InternalServerErrorCode, ErrorMsg: InternalServerErrorMsg})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, HttpResponse{Result: base64.StdEncoding.EncodeToString(raw.Bytes())})
|
||||
return
|
||||
}
|
||||
|
||||
func sourceHandler(c *gin.Context) {
|
||||
driverObj, exists := c.Get("driver")
|
||||
if !exists {
|
||||
log.Error().Msg(fmt.Sprintf("[%s]: Driver Not exsit", c.HandlerName()))
|
||||
c.JSON(http.StatusBadRequest, HttpResponse{Result: "", ErrorCode: InvalidParamErrorCode, ErrorMsg: fmt.Sprintf(InvalidParamErrorMsg, "driver")})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
dExt := driverObj.(*uixt.DriverExt)
|
||||
app, err := dExt.Driver.GetForegroundApp()
|
||||
if err != nil {
|
||||
log.Err(err).Msg(fmt.Sprintf("[%s]: failed to get foreground app", c.HandlerName()))
|
||||
c.JSON(http.StatusInternalServerError, HttpResponse{Result: "", ErrorCode: InternalServerErrorCode, ErrorMsg: InternalServerErrorMsg})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
source, err := dExt.Driver.Source(uixt.NewSourceOption().WithProcessName(app.PackageName))
|
||||
if err != nil {
|
||||
log.Err(err).Msg(fmt.Sprintf("[%s]: failed to get source %s", c.HandlerName(), app.PackageName))
|
||||
c.JSON(http.StatusInternalServerError, HttpResponse{Result: "", ErrorCode: InternalServerErrorCode, ErrorMsg: InternalServerErrorMsg})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, HttpResponse{Result: source})
|
||||
return
|
||||
}
|
||||
|
||||
func adbSourceHandler(c *gin.Context) {
|
||||
deviceObj, exists := c.Get("device")
|
||||
if !exists {
|
||||
log.Error().Msg(fmt.Sprintf("[%s]: Driver Not exsit", c.HandlerName()))
|
||||
c.JSON(http.StatusBadRequest, HttpResponse{Result: "", ErrorCode: InvalidParamErrorCode, ErrorMsg: fmt.Sprintf(InvalidParamErrorMsg, "device")})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
device := deviceObj.(*uixt.AndroidDevice)
|
||||
driver, err := device.NewAdbDriver()
|
||||
if err != nil {
|
||||
log.Err(err).Msg(fmt.Sprintf("[%s]: failed to new adb driver", c.HandlerName()))
|
||||
c.JSON(http.StatusInternalServerError, HttpResponse{Result: "", ErrorCode: InternalServerErrorCode, ErrorMsg: InternalServerErrorMsg})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
source, err := driver.Source()
|
||||
if err != nil {
|
||||
log.Err(err).Msg(fmt.Sprintf("[%s]: failed to get adb source", c.HandlerName()))
|
||||
c.JSON(http.StatusInternalServerError, HttpResponse{Result: "", ErrorCode: InternalServerErrorCode, ErrorMsg: InternalServerErrorMsg})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, HttpResponse{Result: source})
|
||||
return
|
||||
}
|
||||
|
||||
func loginHandler(c *gin.Context) {
|
||||
var loginReq LoginRequest
|
||||
if err := c.ShouldBindJSON(&loginReq); err != nil {
|
||||
log.Err(err).Msg(fmt.Sprintf("[%s]: Invalid Request", c.HandlerName()))
|
||||
c.JSON(http.StatusBadRequest, HttpResponse{Result: "", ErrorCode: InvalidParamErrorCode, ErrorMsg: fmt.Sprintf(InvalidParamErrorMsg, "request")})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
driverObj, exists := c.Get("driver")
|
||||
if !exists {
|
||||
log.Error().Msg(fmt.Sprintf("[%s]: Driver Not exsit", c.HandlerName()))
|
||||
c.JSON(http.StatusBadRequest, HttpResponse{Result: "", ErrorCode: InvalidParamErrorCode, ErrorMsg: fmt.Sprintf(InvalidParamErrorMsg, "driver")})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
dExt := driverObj.(*uixt.DriverExt)
|
||||
err := dExt.Driver.LoginNoneUI(loginReq.PackageName, loginReq.PhoneNumber, loginReq.Captcha)
|
||||
if err != nil {
|
||||
log.Err(err).Msg(fmt.Sprintf("[%s]: failed to get foreground app", c.HandlerName()))
|
||||
c.JSON(http.StatusInternalServerError, HttpResponse{Result: "", ErrorCode: InternalServerErrorCode, ErrorMsg: InternalServerErrorMsg})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, HttpResponse{Result: true})
|
||||
return
|
||||
}
|
||||
|
||||
func parseDeviceInfo() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
platform := c.Param("platform")
|
||||
switch strings.ToLower(platform) {
|
||||
case "android":
|
||||
serial := c.Param("serial")
|
||||
if serial == "" {
|
||||
log.Error().Str("platform", platform).Msg(fmt.Sprintf("[%s]: serial is empty", c.HandlerName()))
|
||||
c.JSON(http.StatusBadRequest, HttpResponse{
|
||||
ErrorCode: InvalidParamErrorCode,
|
||||
ErrorMsg: fmt.Sprintf(InvalidParamErrorMsg, "serial"),
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
device, err := uixt.NewAndroidDevice(uixt.WithSerialNumber(serial), uixt.WithShoots(true))
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("platform", platform).Str("serial", serial).Msg(fmt.Sprintf("[%s]: Device Not Found", c.HandlerName()))
|
||||
c.JSON(http.StatusBadRequest, HttpResponse{
|
||||
ErrorCode: DeviceNotFoundCode,
|
||||
ErrorMsg: fmt.Sprintf(DeviceNotFoundMsg, serial),
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
c.Set("device", device)
|
||||
driver, err := device.NewDriver(uixt.WithDriverImageService(false), uixt.WithDriverResultFolder(false))
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("platform", platform).Str("serial", serial).Msg(fmt.Sprintf("[%s]: Failed New Driver", c.HandlerName()))
|
||||
c.JSON(http.StatusInternalServerError, HttpResponse{
|
||||
ErrorCode: InternalServerErrorCode,
|
||||
ErrorMsg: err.Error(),
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
c.Set("driver", driver)
|
||||
default:
|
||||
c.JSON(http.StatusBadRequest, HttpResponse{
|
||||
ErrorCode: InvalidParamErrorCode,
|
||||
ErrorMsg: fmt.Sprintf(InvalidParamErrorMsg, "platform"),
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,7 @@ const (
|
||||
ACTION_SetClipboard ActionMethod = "set_clipboard"
|
||||
ACTION_GetClipboard ActionMethod = "get_clipboard"
|
||||
ACTION_SetIme ActionMethod = "set_ime"
|
||||
ACTION_GetSource ActionMethod = "get_source"
|
||||
|
||||
// UI validation
|
||||
// selectors
|
||||
@@ -616,6 +617,15 @@ func (dExt *DriverExt) DoAction(action MobileAction) (err error) {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
case ACTION_GetSource:
|
||||
if packageName, ok := action.Params.(string); ok {
|
||||
source := NewSourceOption().WithProcessName(packageName)
|
||||
_, err = dExt.Driver.Source(source)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to set ime")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
case ACTION_TapXY:
|
||||
if location, ok := action.Params.([]interface{}); ok {
|
||||
// relative x,y of window size: [0.5, 0.5]
|
||||
|
||||
@@ -3,6 +3,7 @@ package uixt
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
@@ -218,10 +219,18 @@ func (ad *adbDriver) Orientation() (orientation Orientation, err error) {
|
||||
}
|
||||
|
||||
func (ad *adbDriver) Homescreen() (err error) {
|
||||
return ad.PressKeyCode(KCHome, KMEmpty)
|
||||
return ad.PressKeyCodes(KCHome, KMEmpty)
|
||||
}
|
||||
|
||||
func (ad *adbDriver) PressKeyCode(keyCode KeyCode, metaState KeyMeta) (err error) {
|
||||
func (ad *adbDriver) Unlock() (err error) {
|
||||
return ad.PressKeyCodes(KCMenu, KMEmpty)
|
||||
}
|
||||
|
||||
func (ad *adbDriver) PressKeyCode(keyCode KeyCode) (err error) {
|
||||
return ad.PressKeyCodes(keyCode, KMEmpty)
|
||||
}
|
||||
|
||||
func (ad *adbDriver) PressKeyCodes(keyCode KeyCode, metaState KeyMeta) (err error) {
|
||||
// adb shell input keyevent <keyCode>
|
||||
_, err = ad.adbClient.RunShellCommand(
|
||||
"input", "keyevent", fmt.Sprintf("%d", keyCode))
|
||||
@@ -507,6 +516,10 @@ func (ad *adbDriver) Source(srcOpt ...SourceOption) (source string, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (ad *adbDriver) LoginNoneUI(packageName, phoneNumber string, captcha string) error {
|
||||
return errDriverNotImplemented
|
||||
}
|
||||
|
||||
func (ad *adbDriver) sourceTree(srcOpt ...SourceOption) (sourceTree *Hierarchy, err error) {
|
||||
source, err := ad.Source()
|
||||
if err != nil {
|
||||
@@ -693,51 +706,21 @@ func (ad *adbDriver) GetDriverResults() []*DriverResult {
|
||||
}
|
||||
|
||||
func (ad *adbDriver) GetForegroundApp() (app AppInfo, err error) {
|
||||
// adb shell dumpsys activity activities
|
||||
output, err := ad.adbClient.RunShellCommand("dumpsys", "activity", "activities")
|
||||
packageInfo, err := ad.adbClient.RunShellCommand("CLASSPATH=/data/local/tmp/eval_tool", "app_process", "/", "com.bytedance.iesqa.eval_process.PackageService")
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to dumpsys activities")
|
||||
return AppInfo{}, errors.Wrap(err, "dumpsys activities failed")
|
||||
return app, err
|
||||
}
|
||||
|
||||
lines := strings.Split(string(output), "\n")
|
||||
for _, line := range lines {
|
||||
trimmedLine := strings.TrimSpace(line)
|
||||
// grep mResumedActivity|ResumedActivity
|
||||
if strings.HasPrefix(trimmedLine, "mResumedActivity:") || strings.HasPrefix(trimmedLine, "ResumedActivity:") {
|
||||
// mResumedActivity: ActivityRecord{9656d74 u0 com.android.settings/.Settings t407}
|
||||
// ResumedActivity: ActivityRecord{8265c25 u0 com.android.settings/.Settings t73}
|
||||
strs := strings.Split(trimmedLine, " ")
|
||||
for _, str := range strs {
|
||||
if strings.Contains(str, "/") {
|
||||
// com.android.settings/.Settings
|
||||
s := strings.Split(str, "/")
|
||||
app := AppInfo{
|
||||
AppBaseInfo: AppBaseInfo{
|
||||
PackageName: s[0],
|
||||
Activity: s[1],
|
||||
},
|
||||
}
|
||||
return app, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return AppInfo{}, errors.Wrap(code.MobileUIAssertForegroundAppError, "get foreground app failed")
|
||||
log.Info().Msg(packageInfo)
|
||||
err = json.Unmarshal([]byte(strings.TrimSpace(packageInfo)), &app)
|
||||
return
|
||||
}
|
||||
|
||||
func (ad *adbDriver) GetFocusedPackage() (packageName string, err error) {
|
||||
res, err := ad.adbClient.RunShellCommand("dumpsys", "window", "windows", "|", "grep", "-E", "'mCurrentFocus|mFocusedApp'")
|
||||
res, err := ad.adbClient.RunShellCommand("dumpsys", "activity", "activities", "|", "grep", "-E", "'mResumedActivity'")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
match := regexp.MustCompile("mCurrentFocus.+\\s([^\\s/}]+)/[^\\s/}]+(\\.[^\\s/}]+)}").FindStringSubmatch(res)
|
||||
if len(match) > 1 {
|
||||
packageName = match[1]
|
||||
return
|
||||
}
|
||||
match = regexp.MustCompile("mFocusedApp.+Record\\{.*\\s([^\\s/}]+)/([^\\s/}]+)(\\s[^\\s/}]+)*}").FindStringSubmatch(res)
|
||||
match := regexp.MustCompile(`mResumedActivity:.*? (\S+)/`).FindStringSubmatch(res)
|
||||
if len(match) > 1 {
|
||||
packageName = match[1]
|
||||
return
|
||||
@@ -778,7 +761,7 @@ func (ad *adbDriver) SetIme(imeRegx string) error {
|
||||
currentPackage, err := ad.GetFocusedPackage()
|
||||
log.Info().Str("beforeFocusedPackage", focusedPackage).Str("afterFocusedPackage", currentPackage).Msg("")
|
||||
if err == nil && currentPackage != focusedPackage {
|
||||
_ = ad.PressKeyCode(KCBack, KMEmpty)
|
||||
_ = ad.PressKeyCodes(KCBack, KMEmpty)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,11 @@ import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"embed"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/httprunner/funplugin/myexec"
|
||||
"github.com/pkg/errors"
|
||||
@@ -25,6 +27,9 @@ var (
|
||||
UIA2ServerPort = 6790
|
||||
)
|
||||
|
||||
//go:embed eval_tool
|
||||
var evalTool embed.FS
|
||||
|
||||
const forwardToPrefix = "forward-to-"
|
||||
|
||||
type AndroidDeviceOption func(*AndroidDevice)
|
||||
@@ -41,6 +46,12 @@ func WithUIA2(uia2On bool) AndroidDeviceOption {
|
||||
}
|
||||
}
|
||||
|
||||
func WithShoots(shootsOn bool) AndroidDeviceOption {
|
||||
return func(device *AndroidDevice) {
|
||||
device.SHOOTS = shootsOn
|
||||
}
|
||||
}
|
||||
|
||||
func WithUIA2IP(ip string) AndroidDeviceOption {
|
||||
return func(device *AndroidDevice) {
|
||||
device.UIA2IP = ip
|
||||
@@ -109,7 +120,14 @@ func NewAndroidDevice(options ...AndroidDeviceOption) (device *AndroidDevice, er
|
||||
|
||||
device.d = dev
|
||||
device.logcat = NewAdbLogcat(device.SerialNumber)
|
||||
|
||||
evalToolRaw, err := evalTool.ReadFile("eval_tool")
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(code.LoadFileError, err.Error())
|
||||
}
|
||||
err = dev.Push(bytes.NewReader(evalToolRaw), "/data/local/tmp/eval_tool", time.Now())
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(code.AndroidShellExecError, err.Error())
|
||||
}
|
||||
log.Info().Str("serial", device.SerialNumber).Msg("init android device")
|
||||
return device, nil
|
||||
}
|
||||
@@ -152,6 +170,7 @@ type AndroidDevice struct {
|
||||
logcat *AdbLogcat
|
||||
SerialNumber string `json:"serial,omitempty" yaml:"serial,omitempty"`
|
||||
UIA2 bool `json:"uia2,omitempty" yaml:"uia2,omitempty"` // use uiautomator2
|
||||
SHOOTS bool `json:"shoots,omitempty" yaml:"uia2,omitempty"` // use uiautomator2
|
||||
UIA2IP string `json:"uia2_ip,omitempty" yaml:"uia2_ip,omitempty"` // uiautomator2 server ip
|
||||
UIA2Port int `json:"uia2_port,omitempty" yaml:"uia2_port,omitempty"` // uiautomator2 server port
|
||||
LogOn bool `json:"log_on,omitempty" yaml:"log_on,omitempty"`
|
||||
@@ -174,7 +193,7 @@ func (dev *AndroidDevice) LogEnabled() bool {
|
||||
}
|
||||
|
||||
func (dev *AndroidDevice) NewDriver(options ...DriverOption) (driverExt *DriverExt, err error) {
|
||||
driverOptions := &DriverOptions{}
|
||||
driverOptions := NewDriverOptions()
|
||||
for _, option := range options {
|
||||
option(driverOptions)
|
||||
}
|
||||
@@ -182,6 +201,8 @@ func (dev *AndroidDevice) NewDriver(options ...DriverOption) (driverExt *DriverE
|
||||
var driver WebDriver
|
||||
if dev.UIA2 || dev.LogOn {
|
||||
driver, err = dev.NewUSBDriver(driverOptions.capabilities)
|
||||
} else if dev.SHOOTS {
|
||||
driver, err = dev.NewShootsDriver(driverOptions.capabilities)
|
||||
} else {
|
||||
driver, err = dev.NewAdbDriver()
|
||||
}
|
||||
@@ -189,7 +210,7 @@ func (dev *AndroidDevice) NewDriver(options ...DriverOption) (driverExt *DriverE
|
||||
return nil, errors.Wrap(err, "failed to init UIA driver")
|
||||
}
|
||||
|
||||
driverExt, err = newDriverExt(dev, driver, driverOptions.plugin)
|
||||
driverExt, err = newDriverExt(dev, driver, options...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -226,6 +247,25 @@ func (dev *AndroidDevice) NewUSBDriver(capabilities Capabilities) (driver WebDri
|
||||
return uiaDriver, nil
|
||||
}
|
||||
|
||||
func (dev *AndroidDevice) NewShootsDriver(capabilities Capabilities) (driver *ShootsAndroidDriver, err error) {
|
||||
localPort, err := dev.d.Forward(ShootsSocketName)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(code.AndroidDeviceConnectionError,
|
||||
fmt.Sprintf("forward port %d->%s failed: %v",
|
||||
localPort, ShootsSocketName, err))
|
||||
}
|
||||
|
||||
shootsDriver, err := newShootsAndroidDriver(fmt.Sprintf("127.0.0.1:%d", localPort))
|
||||
if err != nil {
|
||||
_ = dev.d.ForwardKill(localPort)
|
||||
return nil, errors.Wrap(code.AndroidDeviceConnectionError, err.Error())
|
||||
}
|
||||
shootsDriver.adbClient = dev.d
|
||||
shootsDriver.logcat = dev.logcat
|
||||
|
||||
return shootsDriver, nil
|
||||
}
|
||||
|
||||
// NewHTTPDriver creates new remote HTTP client, this will also start a new session.
|
||||
func (dev *AndroidDevice) NewHTTPDriver(capabilities Capabilities) (driver WebDriver, err error) {
|
||||
rawURL := fmt.Sprintf("http://%s:%d/wd/hub", dev.UIA2IP, dev.UIA2Port)
|
||||
|
||||
235
hrp/pkg/uixt/android_shoots_driver.go
Normal file
235
hrp/pkg/uixt/android_shoots_driver.go
Normal file
@@ -0,0 +1,235 @@
|
||||
package uixt
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/httprunner/httprunner/v4/hrp/internal/json"
|
||||
)
|
||||
|
||||
type ShootsAndroidDriver struct {
|
||||
socket net.Conn
|
||||
seq int
|
||||
timeout time.Duration
|
||||
adbDriver
|
||||
}
|
||||
|
||||
const ShootsSocketName = "com.bytest.device"
|
||||
|
||||
// newShootsAndroidDriver
|
||||
// 创建shoots Driver address为forward后的端口格式127.0.0.1:${port}
|
||||
func newShootsAndroidDriver(address string, readTimeout ...time.Duration) (*ShootsAndroidDriver, error) {
|
||||
timeout := 10 * time.Second
|
||||
if len(readTimeout) > 0 {
|
||||
timeout = readTimeout[0]
|
||||
}
|
||||
|
||||
conn, err := net.Dial("tcp", address)
|
||||
if err != nil {
|
||||
log.Err(err).Msg(fmt.Sprintf("failed to connect %s", address))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &ShootsAndroidDriver{
|
||||
socket: conn,
|
||||
timeout: timeout,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (sad *ShootsAndroidDriver) NewSession(capabilities Capabilities) (SessionInfo, error) {
|
||||
return SessionInfo{}, errDriverNotImplemented
|
||||
}
|
||||
|
||||
func (sad *ShootsAndroidDriver) sendCommand(packageName string, cmdType string, params map[string]interface{}, readTimeout ...time.Duration) (interface{}, error) {
|
||||
sad.seq++
|
||||
packet := map[string]interface{}{
|
||||
"Seq": sad.seq,
|
||||
"Cmd": cmdType,
|
||||
"v": "",
|
||||
}
|
||||
for key, value := range params {
|
||||
if key == "Cmd" || key == "Seq" {
|
||||
return "", errors.New("params cannot be Cmd or Seq")
|
||||
}
|
||||
packet[key] = value
|
||||
}
|
||||
data, err := json.Marshal(packet)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res, err := sad.adbClient.RunShootsCommand(append(data, '\n'), packageName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var resultMap map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(res), &resultMap); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resultMap["Error"] != nil {
|
||||
return nil, fmt.Errorf("failed to call shoots command: %s", resultMap["Error"].(string))
|
||||
}
|
||||
|
||||
return resultMap["Result"], nil
|
||||
}
|
||||
|
||||
func (sad *ShootsAndroidDriver) send(data []byte, readTimeout ...time.Duration) (map[string]interface{}, error) {
|
||||
timeout := sad.timeout
|
||||
if len(readTimeout) > 0 {
|
||||
timeout = readTimeout[0]
|
||||
}
|
||||
_ = sad.socket.SetReadDeadline(time.Now().Add(timeout))
|
||||
|
||||
err := _send(sad.socket, append(data, '\n'))
|
||||
if err != nil {
|
||||
sad.close()
|
||||
return nil, err
|
||||
}
|
||||
raw, err := _readAll(sad.socket)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var result map[string]interface{}
|
||||
if err := json.Unmarshal(raw, &result); err != nil {
|
||||
log.Printf("error when parse json response: %s\n", raw)
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func _send(writer io.Writer, msg []byte) (err error) {
|
||||
for totalSent := 0; totalSent < len(msg); {
|
||||
var sent int
|
||||
if sent, err = writer.Write(msg[totalSent:]); err != nil {
|
||||
return err
|
||||
}
|
||||
if sent == 0 {
|
||||
return errors.New("socket connection broken")
|
||||
}
|
||||
totalSent += sent
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func _readN(reader io.Reader, size int) (raw []byte, err error) {
|
||||
raw = make([]byte, 0, size)
|
||||
for len(raw) < size {
|
||||
buf := make([]byte, size-len(raw))
|
||||
var n int
|
||||
if n, err = io.ReadFull(reader, buf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if n == 0 {
|
||||
return nil, errors.New("socket connection broken")
|
||||
}
|
||||
raw = append(raw, buf...)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func _readAll(reader io.Reader) (raw []byte, err error) {
|
||||
buffer := new(bytes.Buffer)
|
||||
for true {
|
||||
lengthBuf := make([]byte, 4)
|
||||
_, err := io.ReadFull(reader, lengthBuf)
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
return buffer.Bytes(), nil
|
||||
} else if errors.Is(err, io.ErrUnexpectedEOF) {
|
||||
err = fmt.Errorf("reached unexpected EOF, read partial data: %s %v", string(buffer.Bytes()), err)
|
||||
return nil, err
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
length := binary.BigEndian.Uint32(lengthBuf)
|
||||
|
||||
data, err := _readN(reader, int(length)-4)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
buffer.Write(data)
|
||||
|
||||
}
|
||||
return buffer.Bytes(), nil
|
||||
}
|
||||
|
||||
func (sad *ShootsAndroidDriver) DeleteSession() error {
|
||||
return sad.close()
|
||||
}
|
||||
|
||||
func (sad *ShootsAndroidDriver) close() error {
|
||||
if sad.socket != nil {
|
||||
return sad.socket.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sad *ShootsAndroidDriver) Status() (DeviceStatus, error) {
|
||||
app, err := sad.GetForegroundApp()
|
||||
res, err := sad.sendCommand(app.PackageName, "Hello", nil)
|
||||
if err != nil {
|
||||
return DeviceStatus{}, err
|
||||
}
|
||||
log.Info().Msg(fmt.Sprintf("pint shoots result :%v", res))
|
||||
return DeviceStatus{}, nil
|
||||
}
|
||||
|
||||
func (sad *ShootsAndroidDriver) Source(srcOpt ...SourceOption) (source string, err error) {
|
||||
app, err := sad.GetForegroundApp()
|
||||
params := map[string]interface{}{
|
||||
"ClassName": "com.bytedance.byteinsight.MockOperator",
|
||||
"Method": "getLayout",
|
||||
"RetType": "",
|
||||
"Args": []string{},
|
||||
}
|
||||
res, err := sad.sendCommand(app.PackageName, "CallStaticMethod", params)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return res.(string), nil
|
||||
}
|
||||
|
||||
func (sad *ShootsAndroidDriver) LoginNoneUI(packageName, phoneNumber string, captcha string) error {
|
||||
_, err := sad.adbClient.RunShellCommand("am", "broadcast", "-a", fmt.Sprintf("%s.util.crony.action_login", packageName), "-e", "phone", phoneNumber, "-e", "code", captcha)
|
||||
time.Sleep(5 * time.Second)
|
||||
login, err := sad.isLogin(packageName)
|
||||
if err != nil || !login {
|
||||
log.Err(err).Msg("failed to login")
|
||||
return fmt.Errorf("failed to login")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (sad *ShootsAndroidDriver) isLogin(packageName string) (login bool, err error) {
|
||||
params := map[string]interface{}{
|
||||
"ClassName": "com.ss.android.ugc.aweme.account.AccountProxyService",
|
||||
"Method": "userService",
|
||||
"RetType": "",
|
||||
"Args": []string{},
|
||||
"CacheObject": true,
|
||||
}
|
||||
id, err := sad.sendCommand(packageName, "CallStaticMethod", params)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
params = map[string]interface{}{
|
||||
"Method": "isLogin",
|
||||
"RetType": "",
|
||||
"Args": []string{},
|
||||
"ObjectId": int(id.(float64)),
|
||||
}
|
||||
loginObj, err := sad.sendCommand(packageName, "CallMethod", params)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return loginObj.(bool), nil
|
||||
}
|
||||
40
hrp/pkg/uixt/android_shoots_driver_test.go
Normal file
40
hrp/pkg/uixt/android_shoots_driver_test.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package uixt
|
||||
|
||||
import "testing"
|
||||
|
||||
var driver *ShootsAndroidDriver
|
||||
|
||||
func setupAndroid(t *testing.T) {
|
||||
device, err := NewAndroidDevice()
|
||||
checkErr(t, err)
|
||||
device.SHOOTS = true
|
||||
driver, err = device.NewShootsDriver(Capabilities{})
|
||||
checkErr(t, err)
|
||||
}
|
||||
|
||||
func TestHello(t *testing.T) {
|
||||
setupAndroid(t)
|
||||
status, err := driver.Status()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(status)
|
||||
}
|
||||
|
||||
func TestSource(t *testing.T) {
|
||||
setupAndroid(t)
|
||||
source, err := driver.Source()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(source)
|
||||
}
|
||||
|
||||
func TestLogin(t *testing.T) {
|
||||
setupAndroid(t)
|
||||
res, err := driver.isLogin("com.ss.android.ugc.aweme")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(res)
|
||||
}
|
||||
@@ -250,10 +250,14 @@ func (ud *uiaDriver) PressBack(options ...ActionOption) (err error) {
|
||||
}
|
||||
|
||||
func (ud *uiaDriver) Homescreen() (err error) {
|
||||
return ud.PressKeyCode(KCHome, KMEmpty)
|
||||
return ud.PressKeyCodes(KCHome, KMEmpty)
|
||||
}
|
||||
|
||||
func (ud *uiaDriver) PressKeyCode(keyCode KeyCode, metaState KeyMeta, flags ...KeyFlag) (err error) {
|
||||
func (ud *uiaDriver) PressKeyCode(keyCode KeyCode) (err error) {
|
||||
return ud.PressKeyCodes(keyCode, KMEmpty)
|
||||
}
|
||||
|
||||
func (ud *uiaDriver) PressKeyCodes(keyCode KeyCode, metaState KeyMeta, flags ...KeyFlag) (err error) {
|
||||
// register(postHandler, new PressKeyCodeAsync("/wd/hub/session/:sessionId/appium/device/press_keycode"))
|
||||
data := map[string]interface{}{
|
||||
"keycode": keyCode,
|
||||
|
||||
BIN
hrp/pkg/uixt/eval_tool
Normal file
BIN
hrp/pkg/uixt/eval_tool
Normal file
Binary file not shown.
@@ -155,11 +155,16 @@ type DriverExt struct {
|
||||
plugin funplugin.IPlugin
|
||||
}
|
||||
|
||||
func newDriverExt(device Device, driver WebDriver, plugin funplugin.IPlugin) (dExt *DriverExt, err error) {
|
||||
func newDriverExt(device Device, driver WebDriver, options ...DriverOption) (dExt *DriverExt, err error) {
|
||||
driverOptions := &DriverOptions{}
|
||||
for _, option := range options {
|
||||
option(driverOptions)
|
||||
}
|
||||
|
||||
dExt = &DriverExt{
|
||||
Device: device,
|
||||
Driver: driver,
|
||||
plugin: plugin,
|
||||
plugin: driverOptions.plugin,
|
||||
cacheStepData: cacheStepData{},
|
||||
interruptSignal: make(chan os.Signal, 1),
|
||||
}
|
||||
@@ -179,17 +184,19 @@ func newDriverExt(device Device, driver WebDriver, plugin funplugin.IPlugin) (dE
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "get screen resolution failed")
|
||||
}
|
||||
|
||||
if dExt.ImageService, err = newVEDEMImageService(); err != nil {
|
||||
return nil, err
|
||||
if driverOptions.withImageService {
|
||||
if dExt.ImageService, err = newVEDEMImageService(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// create results directory
|
||||
if err = builtin.EnsureFolderExists(env.ResultsPath); err != nil {
|
||||
return nil, errors.Wrap(err, "create results directory failed")
|
||||
}
|
||||
if err = builtin.EnsureFolderExists(env.ScreenShotsPath); err != nil {
|
||||
return nil, errors.Wrap(err, "create screenshots directory failed")
|
||||
if driverOptions.withResultFolder {
|
||||
// create results directory
|
||||
if err = builtin.EnsureFolderExists(env.ResultsPath); err != nil {
|
||||
return nil, errors.Wrap(err, "create results directory failed")
|
||||
}
|
||||
if err = builtin.EnsureFolderExists(env.ScreenShotsPath); err != nil {
|
||||
return nil, errors.Wrap(err, "create screenshots directory failed")
|
||||
}
|
||||
}
|
||||
return dExt, nil
|
||||
}
|
||||
|
||||
@@ -252,11 +252,7 @@ type Screen struct {
|
||||
}
|
||||
|
||||
type AppInfo struct {
|
||||
ProcessArguments struct {
|
||||
Env interface{} `json:"env"`
|
||||
Args []interface{} `json:"args"`
|
||||
} `json:"processArguments"`
|
||||
Name string `json:"name"`
|
||||
Name string `json:"name,omitempty"`
|
||||
AppBaseInfo
|
||||
}
|
||||
|
||||
@@ -266,6 +262,10 @@ type AppBaseInfo struct {
|
||||
ViewController string `json:"viewController,omitempty"` // ios view controller
|
||||
PackageName string `json:"packageName,omitempty"` // android package name
|
||||
Activity string `json:"activity,omitempty"` // android activity
|
||||
VersionName string `json:"versionName,omitempty"`
|
||||
VersionCode int `json:"versionCode,omitempty"`
|
||||
AppName string `json:"appName,omitempty"`
|
||||
// AppIcon string `json:"appIcon,omitempty"`
|
||||
}
|
||||
|
||||
type AppState int
|
||||
@@ -376,6 +376,11 @@ func (opt SourceOption) WithFormatAsJson() SourceOption {
|
||||
return opt
|
||||
}
|
||||
|
||||
func (opt SourceOption) WithProcessName(processName string) SourceOption {
|
||||
opt["processName"] = processName
|
||||
return opt
|
||||
}
|
||||
|
||||
// WithFormatAsXml Application elements tree in form of xml string
|
||||
func (opt SourceOption) WithFormatAsXml() SourceOption {
|
||||
opt["format"] = "xml"
|
||||
@@ -448,8 +453,17 @@ type Rect struct {
|
||||
}
|
||||
|
||||
type DriverOptions struct {
|
||||
capabilities Capabilities
|
||||
plugin funplugin.IPlugin
|
||||
capabilities Capabilities
|
||||
plugin funplugin.IPlugin
|
||||
withImageService bool
|
||||
withResultFolder bool
|
||||
}
|
||||
|
||||
func NewDriverOptions() *DriverOptions {
|
||||
return &DriverOptions{
|
||||
withImageService: true,
|
||||
withResultFolder: true,
|
||||
}
|
||||
}
|
||||
|
||||
type DriverOption func(*DriverOptions)
|
||||
@@ -460,6 +474,18 @@ func WithDriverCapabilities(capabilities Capabilities) DriverOption {
|
||||
}
|
||||
}
|
||||
|
||||
func WithDriverImageService(withImageService bool) DriverOption {
|
||||
return func(options *DriverOptions) {
|
||||
options.withImageService = withImageService
|
||||
}
|
||||
}
|
||||
|
||||
func WithDriverResultFolder(withResultFolder bool) DriverOption {
|
||||
return func(options *DriverOptions) {
|
||||
options.withResultFolder = withResultFolder
|
||||
}
|
||||
}
|
||||
|
||||
func WithDriverPlugin(plugin funplugin.IPlugin) DriverOption {
|
||||
return func(options *DriverOptions) {
|
||||
options.plugin = plugin
|
||||
@@ -526,6 +552,8 @@ type WebDriver interface {
|
||||
// Homescreen Forces the device under test to switch to the home screen
|
||||
Homescreen() error
|
||||
|
||||
Unlock() (err error)
|
||||
|
||||
// AppLaunch Launch an application with given bundle identifier in scope of current session.
|
||||
// !This method is only available since Xcode9 SDK
|
||||
AppLaunch(packageName string) error
|
||||
@@ -588,11 +616,15 @@ type WebDriver interface {
|
||||
// PressBack Presses the back button
|
||||
PressBack(options ...ActionOption) error
|
||||
|
||||
PressKeyCode(keyCode KeyCode) (err error)
|
||||
|
||||
Screenshot() (*bytes.Buffer, error)
|
||||
|
||||
// Source Return application elements tree
|
||||
Source(srcOpt ...SourceOption) (string, error)
|
||||
|
||||
LoginNoneUI(packageName, phoneNumber string, captcha string) error
|
||||
|
||||
TapByText(text string, options ...ActionOption) error
|
||||
|
||||
TapByTexts(actions ...TapTextAction) error
|
||||
|
||||
@@ -308,7 +308,7 @@ func (dev *IOSDevice) LogEnabled() bool {
|
||||
}
|
||||
|
||||
func (dev *IOSDevice) NewDriver(options ...DriverOption) (driverExt *DriverExt, err error) {
|
||||
driverOptions := &DriverOptions{}
|
||||
driverOptions := NewDriverOptions()
|
||||
for _, option := range options {
|
||||
option(driverOptions)
|
||||
}
|
||||
@@ -339,7 +339,7 @@ func (dev *IOSDevice) NewDriver(options ...DriverOption) (driverExt *DriverExt,
|
||||
}
|
||||
}
|
||||
|
||||
driverExt, err = newDriverExt(dev, driver, driverOptions.plugin)
|
||||
driverExt, err = newDriverExt(dev, driver, options...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -595,6 +595,10 @@ func (wd *wdaDriver) SetIme(ime string) error {
|
||||
return errDriverNotImplemented
|
||||
}
|
||||
|
||||
func (wd *wdaDriver) PressKeyCode(keyCode KeyCode) (err error) {
|
||||
return errDriverNotImplemented
|
||||
}
|
||||
|
||||
func (wd *wdaDriver) SendKeys(text string, options ...ActionOption) (err error) {
|
||||
// [[FBRoute POST:@"/wda/keys"] respondWithTarget:self action:@selector(handleKeys:)]
|
||||
actionOptions := NewActionOptions(options...)
|
||||
@@ -655,6 +659,10 @@ func (wd *wdaDriver) PressButton(devBtn DeviceButton) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (wd *wdaDriver) LoginNoneUI(packageName, phoneNumber string, captcha string) error {
|
||||
return errDriverNotImplemented
|
||||
}
|
||||
|
||||
func (wd *wdaDriver) StartCamera() (err error) {
|
||||
// start camera, alias for app_launch com.apple.camera
|
||||
return wd.AppLaunch("com.apple.camera")
|
||||
|
||||
Reference in New Issue
Block a user