From 878b626971b8ba5ef2be123b623fa691f53ee37f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BD=99=E6=B3=93=E9=93=AE?= Date: Wed, 18 Sep 2024 13:44:54 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E9=B8=BF=E8=92=99?= =?UTF-8?q?=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go.mod | 5 +- go.sum | 2 + hrp/pkg/uixt/harmony_device.go | 158 ++++++++++++++ hrp/pkg/uixt/harmony_hdc_driver.go | 317 +++++++++++++++++++++++++++++ hrp/pkg/uixt/harmony_test.go | 109 ++++++++++ hrp/step_mobile_ui.go | 2 + 6 files changed, 592 insertions(+), 1 deletion(-) create mode 100644 hrp/pkg/uixt/harmony_device.go create mode 100644 hrp/pkg/uixt/harmony_hdc_driver.go create mode 100644 hrp/pkg/uixt/harmony_test.go diff --git a/go.mod b/go.mod index 63cc82fd..da91ad5c 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,8 @@ module github.com/httprunner/httprunner/v4 -go 1.18 +go 1.22 + +toolchain go1.22.0 require ( github.com/andybalholm/brotli v1.0.4 @@ -39,6 +41,7 @@ require ( require ( cloud.google.com/go/compute v1.23.0 // indirect cloud.google.com/go/compute/metadata v0.2.3 // indirect + code.byted.org/iesqa/ghdc v0.0.0-20240911080657-3fe04d3190a5 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bytedance/sonic v1.11.6 // indirect github.com/bytedance/sonic/loader v0.1.1 // indirect diff --git a/go.sum b/go.sum index 270107b7..5c675132 100644 --- a/go.sum +++ b/go.sum @@ -34,6 +34,8 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +code.byted.org/iesqa/ghdc v0.0.0-20240911080657-3fe04d3190a5 h1:9H06vi9l4K8xjhQg5Lsu4lbtB2NBKUG/l4XyRGhLuAk= +code.byted.org/iesqa/ghdc v0.0.0-20240911080657-3fe04d3190a5/go.mod h1:0IrKgKT75jmlpi9N0Mi5xWKctJuKHFM6f+ZMQe5vnNs= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= diff --git a/hrp/pkg/uixt/harmony_device.go b/hrp/pkg/uixt/harmony_device.go new file mode 100644 index 00000000..fc333c77 --- /dev/null +++ b/hrp/pkg/uixt/harmony_device.go @@ -0,0 +1,158 @@ +package uixt + +import ( + "fmt" + + "code.byted.org/iesqa/ghdc" + "github.com/pkg/errors" + "github.com/rs/zerolog/log" + + "github.com/httprunner/httprunner/v4/hrp/code" +) + +var ( + HdcServerHost = "localhost" + HdcServerPort = ghdc.HdcServerPort // 5037 +) + +type HarmonyDevice struct { + d *ghdc.Device + ConnectKey string `json:"connect_key,omitempty" yaml:"connect_key,omitempty"` + IgnorePopup bool `json:"ignore_popup,omitempty" yaml:"ignore_popup,omitempty"` +} + +type HarmonyDeviceOption func(*HarmonyDevice) + +func WithConnectKey(connectKey string) HarmonyDeviceOption { + return func(device *HarmonyDevice) { + device.ConnectKey = connectKey + } +} + +func NewHarmonyDevice(options ...HarmonyDeviceOption) (device *HarmonyDevice, err error) { + device = &HarmonyDevice{} + for _, option := range options { + option(device) + } + + deviceList, err := GetHarmonyDevices(device.ConnectKey) + if err != nil { + return nil, errors.Wrap(code.DeviceConnectionError, err.Error()) + } + + if device.ConnectKey == "" && len(deviceList) > 1 { + return nil, errors.Wrap(code.DeviceConnectionError, "more than one device connected, please specify the serial") + } + + dev := deviceList[0] + + if device.ConnectKey == "" { + selectSerial := dev.Serial() + device.ConnectKey = selectSerial + log.Warn(). + Str("connectKey", device.ConnectKey). + Msg("harmony ConnectKey is not specified, select the first one") + } + + device.d = dev + log.Info().Str("connectKey", device.ConnectKey).Msg("init harmony device") + return device, nil +} + +func GetHarmonyDevices(serial ...string) (devices []*ghdc.Device, err error) { + var hdcClient ghdc.Client + if hdcClient, err = ghdc.NewClientWith(HdcServerHost, HdcServerPort); err != nil { + return nil, err + } + var deviceList []ghdc.Device + + if deviceList, err = hdcClient.DeviceList(); err != nil { + return nil, err + } + + // filter by serial + for _, d := range deviceList { + for _, s := range serial { + if s != "" && s != d.Serial() { + continue + } + devices = append(devices, &d) + } + } + + if len(devices) == 0 { + var err error + if serial == nil || (len(serial) == 1 && serial[0] == "") { + err = fmt.Errorf("no harmony device found") + } else { + err = fmt.Errorf("no harmony device found for serial %v", serial) + } + return nil, err + } + return devices, nil +} + +func (dev *HarmonyDevice) Init() error { + return nil +} + +func (dev *HarmonyDevice) UUID() string { + return dev.ConnectKey +} + +func (dev *HarmonyDevice) LogEnabled() bool { + return false +} + +func (dev *HarmonyDevice) NewDriver(options ...DriverOption) (driverExt *DriverExt, err error) { + driver, err := newHarmonyDriver(dev.d) + if err != nil { + log.Error().Err(err).Msg("failed to new harmony driver") + return nil, err + } + + driverExt, err = newDriverExt(dev, driver, options...) + if err != nil { + return nil, err + } + + return driverExt, nil +} + +func (dev *HarmonyDevice) NewUSBDriver(options ...DriverOption) (driver IWebDriver, err error) { + harmonyDriver, err := newHarmonyDriver(dev.d) + if err != nil { + log.Error().Err(err).Msg("failed to new harmony driver") + return nil, err + } + + return harmonyDriver, nil +} + +func (dev *HarmonyDevice) StartPerf() error { + return nil +} + +func (dev *HarmonyDevice) StopPerf() string { + return "" +} + +func (dev *HarmonyDevice) StartPcap() error { + return nil +} + +func (dev *HarmonyDevice) StopPcap() string { + return "" +} + +func (dev *HarmonyDevice) Install(appPath string, opts *InstallOptions) error { + return nil +} + +func (dev *HarmonyDevice) Uninstall(packageName string) error { + return nil +} + +func (dev *HarmonyDevice) GetPackageInfo(packageName string) (AppInfo, error) { + return AppInfo{}, nil +} diff --git a/hrp/pkg/uixt/harmony_hdc_driver.go b/hrp/pkg/uixt/harmony_hdc_driver.go new file mode 100644 index 00000000..0968d2c4 --- /dev/null +++ b/hrp/pkg/uixt/harmony_hdc_driver.go @@ -0,0 +1,317 @@ +package uixt + +import ( + "bytes" + "fmt" + "os" + "regexp" + "strconv" + "time" + + "code.byted.org/iesqa/ghdc" + "github.com/rs/zerolog/log" +) + +type hdcDriver struct { + Driver + device *ghdc.Device + uiDriver *ghdc.UIDriver +} + +func newHarmonyDriver(device *ghdc.Device) (driver *hdcDriver, err error) { + driver = new(hdcDriver) + driver.device = device + uiDriver, err := ghdc.NewUIDriver(*device) + if err != nil { + log.Error().Err(err).Msg("failed to new harmony ui driver") + return nil, err + } + driver.uiDriver = uiDriver + return +} + +func (hd *hdcDriver) NewSession(capabilities Capabilities) (SessionInfo, error) { + return SessionInfo{}, errDriverNotImplemented +} + +func (hd *hdcDriver) DeleteSession() error { + return errDriverNotImplemented +} + +func (hd *hdcDriver) GetSession() *DriverSession { + return nil +} + +func (hd *hdcDriver) Status() (DeviceStatus, error) { + return DeviceStatus{}, errDriverNotImplemented +} + +func (hd *hdcDriver) DeviceInfo() (DeviceInfo, error) { + return DeviceInfo{}, errDriverNotImplemented +} + +func (hd *hdcDriver) Location() (Location, error) { + return Location{}, errDriverNotImplemented +} + +func (hd *hdcDriver) BatteryInfo() (BatteryInfo, error) { + return BatteryInfo{}, errDriverNotImplemented +} + +func (hd *hdcDriver) WindowSize() (size Size, err error) { + res, err := hd.device.RunShellCommand("hidumper", "-s", "RenderService", "-a", "screen") + if err != nil { + log.Error().Err(err).Msg("failed to get window size") + return size, err + } + re := regexp.MustCompile(`activeMode:\s*(\d+)x(\d+)`) + matches := re.FindStringSubmatch(res) + + if len(matches) > 2 { + fmt.Printf("Width: %s, Height: %s\n", matches[1], matches[2]) + width, err := strconv.Atoi(matches[1]) + if err != nil { + log.Error().Err(err).Str("width", matches[1]).Msg("failed to get window size") + return size, err + } + size.Width = width + height, err := strconv.Atoi(matches[2]) + if err != nil { + log.Error().Err(err).Str("height", matches[2]).Msg("failed to get window size") + return size, err + } + size.Height = height + return size, nil + } + err = fmt.Errorf("failed to find window size in dump result") + log.Error().Err(err).Str("result", res).Msg("failed to get window size") + return size, err +} + +func (hd *hdcDriver) Screen() (Screen, error) { + return Screen{}, errDriverNotImplemented +} + +func (hd *hdcDriver) Scale() (float64, error) { + return 1, nil +} + +func (hd *hdcDriver) GetTimestamp() (timestamp int64, err error) { + return 0, errDriverNotImplemented +} + +func (hd *hdcDriver) Homescreen() error { + return hd.uiDriver.PressKey(ghdc.KEYCODE_HOME) +} + +func (hd *hdcDriver) Unlock() (err error) { + return hd.uiDriver.PressKey(ghdc.KEYCODE_HOME) +} + +func (hd *hdcDriver) AppLaunch(packageName string) error { + // Todo + return errDriverNotImplemented +} + +func (hd *hdcDriver) AppTerminate(packageName string) (bool, error) { + // Todo + return false, errDriverNotImplemented +} + +func (hd *hdcDriver) GetForegroundApp() (app AppInfo, err error) { + // Todo + return AppInfo{}, errDriverNotImplemented +} + +func (hd *hdcDriver) AssertForegroundApp(packageName string, activityType ...string) error { + // Todo + return errDriverNotImplemented +} + +func (hd *hdcDriver) StartCamera() error { + return errDriverNotImplemented +} + +func (hd *hdcDriver) StopCamera() error { + return errDriverNotImplemented +} + +func (hd *hdcDriver) Orientation() (orientation Orientation, err error) { + return OrientationPortrait, nil +} + +func (hd *hdcDriver) Tap(x, y int, options ...ActionOption) error { + return hd.TapFloat(float64(x), float64(y), options...) +} + +func (hd *hdcDriver) TapFloat(x, y float64, options ...ActionOption) error { + actionOptions := NewActionOptions(options...) + + if len(actionOptions.Offset) == 2 { + x += float64(actionOptions.Offset[0]) + y += float64(actionOptions.Offset[1]) + } + + x += actionOptions.getRandomOffset() + y += actionOptions.getRandomOffset() + + return hd.uiDriver.Touch(int(x), int(y)) +} + +func (hd *hdcDriver) DoubleTap(x, y int, options ...ActionOption) error { + return errDriverNotImplemented +} + +func (hd *hdcDriver) DoubleTapFloat(x, y float64, options ...ActionOption) error { + return errDriverNotImplemented +} + +func (hd *hdcDriver) TouchAndHold(x, y int, second ...float64) error { + return errDriverNotImplemented +} + +func (hd *hdcDriver) TouchAndHoldFloat(x, y float64, second ...float64) error { + return errDriverNotImplemented +} + +func (hd *hdcDriver) Drag(fromX, fromY, toX, toY int, options ...ActionOption) error { + return errDriverNotImplemented +} + +func (hd *hdcDriver) DragFloat(fromX, fromY, toX, toY float64, options ...ActionOption) error { + return errDriverNotImplemented +} + +// Swipe works like Drag, but `pressForDuration` value is 0 +func (hd *hdcDriver) Swipe(fromX, fromY, toX, toY int, options ...ActionOption) error { + return hd.SwipeFloat(float64(fromX), float64(fromY), float64(toX), float64(toY), options...) +} + +func (hd *hdcDriver) SwipeFloat(fromX, fromY, toX, toY float64, options ...ActionOption) error { + actionOptions := NewActionOptions(options...) + if len(actionOptions.Offset) == 4 { + fromX += float64(actionOptions.Offset[0]) + fromY += float64(actionOptions.Offset[1]) + toX += float64(actionOptions.Offset[2]) + toY += float64(actionOptions.Offset[3]) + } + fromX += actionOptions.getRandomOffset() + fromY += actionOptions.getRandomOffset() + toX += actionOptions.getRandomOffset() + toY += actionOptions.getRandomOffset() + + duration := 0.2 + if actionOptions.PressDuration > 0 { + duration = actionOptions.PressDuration + } + + return hd.uiDriver.Drag(int(fromX), int(fromY), int(toX), int(toY), duration) +} + +func (hd *hdcDriver) SetPasteboard(contentType PasteboardType, content string) error { + return errDriverNotImplemented +} + +func (hd *hdcDriver) GetPasteboard(contentType PasteboardType) (raw *bytes.Buffer, err error) { + return nil, errDriverNotImplemented +} + +func (hd *hdcDriver) SetIme(ime string) error { + return errDriverNotImplemented +} + +func (hd *hdcDriver) SendKeys(text string, options ...ActionOption) error { + return hd.uiDriver.InputText(text) +} + +func (hd *hdcDriver) Input(text string, options ...ActionOption) error { + return hd.uiDriver.InputText(text) +} + +func (hd *hdcDriver) Clear(packageName string) error { + return errDriverNotImplemented +} + +func (hd *hdcDriver) PressButton(devBtn DeviceButton) error { + return errDriverNotImplemented +} + +func (hd *hdcDriver) PressBack(options ...ActionOption) error { + return hd.uiDriver.PressBack() +} + +func (hd *hdcDriver) PressKeyCode(keyCode KeyCode) (err error) { + return errDriverNotImplemented +} + +func (hd *hdcDriver) PressHarmonyKeyCode(keyCode ghdc.KeyCode) (err error) { + return hd.uiDriver.PressKey(keyCode) +} + +func (hd *hdcDriver) Screenshot() (*bytes.Buffer, error) { + tempDir := os.TempDir() + screenshotPath := fmt.Sprintf("%s/screenshot_%d.png", tempDir, time.Now().Unix()) + err := hd.uiDriver.Screenshot(screenshotPath) + if err != nil { + log.Error().Err(err).Msg("failed to screenshot") + return nil, err + } + defer func() { + _ = os.Remove(screenshotPath) + }() + + raw, err := os.ReadFile(screenshotPath) + if err != nil { + log.Error().Err(err).Msg("failed to screenshot") + return nil, err + } + return bytes.NewBuffer(raw), nil +} + +func (hd *hdcDriver) Source(srcOpt ...SourceOption) (string, error) { + return "", nil +} + +func (hd *hdcDriver) LoginNoneUI(packageName, phoneNumber string, captcha string) error { + return errDriverNotImplemented +} + +func (hd *hdcDriver) LogoutNoneUI(packageName string) error { + return errDriverNotImplemented +} + +func (hd *hdcDriver) TapByText(text string, options ...ActionOption) error { + return errDriverNotImplemented +} + +func (hd *hdcDriver) TapByTexts(actions ...TapTextAction) error { + return errDriverNotImplemented +} + +func (hd *hdcDriver) AccessibleSource() (string, error) { + return "", errDriverNotImplemented +} + +func (hd *hdcDriver) HealthCheck() error { + return errDriverNotImplemented +} + +func (hd *hdcDriver) GetAppiumSettings() (map[string]interface{}, error) { + return nil, errDriverNotImplemented +} + +func (hd *hdcDriver) SetAppiumSettings(settings map[string]interface{}) (map[string]interface{}, error) { + return nil, errDriverNotImplemented +} + +func (hd *hdcDriver) IsHealthy() (bool, error) { + return false, errDriverNotImplemented +} + +func (hd *hdcDriver) StartCaptureLog(identifier ...string) (err error) { + return errDriverNotImplemented +} + +func (hd *hdcDriver) StopCaptureLog() (result interface{}, err error) { + return nil, errDriverNotImplemented +} diff --git a/hrp/pkg/uixt/harmony_test.go b/hrp/pkg/uixt/harmony_test.go new file mode 100644 index 00000000..dbb82134 --- /dev/null +++ b/hrp/pkg/uixt/harmony_test.go @@ -0,0 +1,109 @@ +package uixt + +import ( + "fmt" + "testing" +) + +var ( + driver IWebDriver + harmonyDriverExt *DriverExt +) + +func setup(t *testing.T) { + device, err := NewHarmonyDevice() + if err != nil { + t.Fatal(err) + } + driver, err = device.NewUSBDriver() + if err != nil { + t.Fatal(err) + } + harmonyDriverExt, err = newDriverExt(device, driver) + if err != nil { + t.Fatal(err) + } +} + +func TestWindowSize(t *testing.T) { + setup(t) + size, err := driver.WindowSize() + if err != nil { + t.Fatal(err) + } + t.Log(fmt.Sprintf("width: %d, height: %d", size.Width, size.Height)) +} + +func TestTap(t *testing.T) { + setup(t) + err := harmonyDriverExt.TapAbsXY(200, 2000) + if err != nil { + t.Fatal(err) + } +} + +func TestSwipe(t *testing.T) { + setup(t) + err := harmonyDriverExt.SwipeLeft() + if err != nil { + t.Fatal(err) + } +} + +func TestInput(t *testing.T) { + setup(t) + err := harmonyDriverExt.Input("test测试123!@#") + if err != nil { + t.Fatal(err) + } +} + +func TestHomeScreen(t *testing.T) { + setup(t) + err := driver.Homescreen() + if err != nil { + t.Fatal(err) + } +} + +func TestUnlock(t *testing.T) { + setup(t) + err := driver.Unlock() + if err != nil { + t.Fatal(err) + } +} + +func TestPressBack(t *testing.T) { + setup(t) + err := driver.PressBack() + if err != nil { + t.Fatal(err) + } +} + +func TestScreenshot(t *testing.T) { + setup(t) + screenshot, err := driver.Screenshot() + if err != nil { + t.Fatal(err) + } + t.Log(screenshot) +} + +func TestLaunch(t *testing.T) { + setup(t) + err := driver.AppLaunch("") + if err != nil { + t.Fatal(err) + } +} + +func TestForegroundApp(t *testing.T) { + setup(t) + appInfo, err := driver.GetForegroundApp() + if err != nil { + t.Fatal(err) + } + t.Log(appInfo) +} diff --git a/hrp/step_mobile_ui.go b/hrp/step_mobile_ui.go index 8c22570d..2b611c7b 100644 --- a/hrp/step_mobile_ui.go +++ b/hrp/step_mobile_ui.go @@ -35,6 +35,8 @@ func initUIClient(serial, osType string) (client *uixt.DriverExt, err error) { var device uixt.IDevice if osType == "ios" { device, err = uixt.NewIOSDevice(uixt.WithUDID(serial)) + } else if osType == "harmony" { + device, err = uixt.NewHarmonyDevice(uixt.WithConnectKey(serial)) } else { device, err = uixt.NewAndroidDevice(uixt.WithSerialNumber(serial)) }