diff --git a/go.mod b/go.mod index 63cc82fd..f3b87bfb 100644 --- a/go.mod +++ b/go.mod @@ -39,6 +39,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-20240918093157-b4feef0e5af0 // 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..4a624d15 100644 --- a/go.sum +++ b/go.sum @@ -34,6 +34,14 @@ 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= +code.byted.org/iesqa/ghdc v0.0.0-20240918062858-e57e2d72ed7b h1:pbIbB1S+vhIgEeaDoIqM5GtsCtlGXYaO7VZBYbVGYZU= +code.byted.org/iesqa/ghdc v0.0.0-20240918062858-e57e2d72ed7b/go.mod h1:0IrKgKT75jmlpi9N0Mi5xWKctJuKHFM6f+ZMQe5vnNs= +code.byted.org/iesqa/ghdc v0.0.0-20240918083005-02dc9c3eed10 h1:QwIVe4NaY3i3u0sN3JGczfrtAlI0FnPQSfOCfojudoc= +code.byted.org/iesqa/ghdc v0.0.0-20240918083005-02dc9c3eed10/go.mod h1:0IrKgKT75jmlpi9N0Mi5xWKctJuKHFM6f+ZMQe5vnNs= +code.byted.org/iesqa/ghdc v0.0.0-20240918093157-b4feef0e5af0 h1:qsKGQS3A530QpOXY80ogzzhVpf25Q4WfHusSlRyNvLU= +code.byted.org/iesqa/ghdc v0.0.0-20240918093157-b4feef0e5af0/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..8820c962 --- /dev/null +++ b/hrp/pkg/uixt/harmony_device.go @@ -0,0 +1,171 @@ +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"` + LogOn bool `json:"log_on,omitempty" yaml:"log_on,omitempty"` +} + +type HarmonyDeviceOption func(*HarmonyDevice) + +func WithConnectKey(connectKey string) HarmonyDeviceOption { + return func(device *HarmonyDevice) { + device.ConnectKey = connectKey + } +} + +func WithIgnorePopup(ignorePopup bool) HarmonyDeviceOption { + return func(device *HarmonyDevice) { + device.IgnorePopup = ignorePopup + } +} + +func WithLogOn(logOn bool) HarmonyDeviceOption { + return func(device *HarmonyDevice) { + device.LogOn = logOn + } +} + +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 dev.LogOn +} + +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..7aa89d8f --- /dev/null +++ b/hrp/pkg/uixt/harmony_hdc_driver.go @@ -0,0 +1,315 @@ +package uixt + +import ( + "bytes" + "fmt" + "os" + "time" + + "code.byted.org/iesqa/ghdc" + "github.com/rs/zerolog/log" +) + +type hdcDriver struct { + points []ExportPoint + 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 + driver.NewSession(nil) + return +} + +func (hd *hdcDriver) NewSession(capabilities Capabilities) (SessionInfo, error) { + hd.Driver.session.Init() + return SessionInfo{}, errDriverNotImplemented +} + +func (hd *hdcDriver) DeleteSession() error { + return errDriverNotImplemented +} + +func (hd *hdcDriver) GetSession() *DriverSession { + return &hd.Driver.session +} + +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) { + display, err := hd.uiDriver.GetDisplaySize() + if err != nil { + log.Error().Err(err).Msg("failed to get window size") + return Size{}, err + } + size.Width = display.Width + size.Height = display.Height + 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) { + // Todo 检查是否锁屏 hdc shell hidumper -s RenderService -a screen + err = hd.uiDriver.PressPowerKey() + if err != nil { + return err + } + return hd.Swipe(500, 2000, 500, 500) +} + +func (hd *hdcDriver) AppLaunch(packageName string) error { + // Todo + return errDriverNotImplemented +} + +func (hd *hdcDriver) AppTerminate(packageName string) (bool, error) { + _, err := hd.device.RunShellCommand("aa", "force-stop", packageName) + if err != nil { + log.Error().Err(err).Msg("failed to terminal app") + return false, err + } + return true, nil +} + +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() + if actionOptions.Identifier != "" { + startTime := int(time.Now().UnixMilli()) + hd.points = append(hd.points, ExportPoint{Start: startTime, End: startTime + 100, Ext: actionOptions.Identifier, RunTime: 100}) + } + return hd.uiDriver.InjectGesture(ghdc.NewGesture().Start(ghdc.Point{X: int(x), Y: int(y)}).Pause(100)) +} + +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 := 200 + if actionOptions.PressDuration > 0 { + duration = int(actionOptions.PressDuration * 1000) + } + if actionOptions.Identifier != "" { + startTime := int(time.Now().UnixMilli()) + hd.points = append(hd.points, ExportPoint{Start: startTime, End: startTime + 100, Ext: actionOptions.Identifier, RunTime: 100}) + } + return hd.uiDriver.InjectGesture(ghdc.NewGesture().Start(ghdc.Point{X: int(fromX), Y: int(fromY)}).MoveTo(ghdc.Point{X: int(toX), Y: 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) { + // defer clear(hd.points) + return hd.points, nil +} diff --git a/hrp/pkg/uixt/harmony_test.go b/hrp/pkg/uixt/harmony_test.go new file mode 100644 index 00000000..b47eb9c4 --- /dev/null +++ b/hrp/pkg/uixt/harmony_test.go @@ -0,0 +1,111 @@ +//go:build localtest + +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 TestHarmonyTap(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") + 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.go b/hrp/step.go index 328f8343..f0c69f35 100644 --- a/hrp/step.go +++ b/hrp/step.go @@ -11,6 +11,7 @@ const ( stepTypeThinkTime StepType = "thinktime" stepTypeWebSocket StepType = "websocket" stepTypeAndroid StepType = "android" + stepTypeHarmony StepType = "harmony" stepTypeIOS StepType = "ios" stepTypeShell StepType = "shell" @@ -54,6 +55,7 @@ type TStep struct { ThinkTime *ThinkTime `json:"think_time,omitempty" yaml:"think_time,omitempty"` WebSocket *WebSocketAction `json:"websocket,omitempty" yaml:"websocket,omitempty"` Android *MobileUI `json:"android,omitempty" yaml:"android,omitempty"` + Harmony *MobileUI `json:"harmony,omitempty" yaml:"harmony,omitempty"` IOS *MobileUI `json:"ios,omitempty" yaml:"ios,omitempty"` Shell *Shell `json:"shell,omitempty" yaml:"shell,omitempty"` Variables map[string]interface{} `json:"variables,omitempty" yaml:"variables,omitempty"` diff --git a/hrp/step_mobile_ui.go b/hrp/step_mobile_ui.go index 8c22570d..1930b71c 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)) } @@ -396,6 +398,8 @@ func (s *StepMobile) Name() string { func (s *StepMobile) Type() StepType { if s.step.Android != nil { return stepTypeAndroid + } else if s.step.Harmony != nil { + return stepTypeHarmony } return stepTypeIOS } @@ -589,10 +593,13 @@ func runStepMobileUI(s *SessionRunner, step *TStep) (stepResult *StepResult, err // ios step osType = "ios" mobileStep = step.IOS - } else { + } else if step.Android != nil { // android step osType = "android" mobileStep = step.Android + } else { + osType = "harmony" + mobileStep = step.Harmony } // report GA event diff --git a/hrp/testcase.go b/hrp/testcase.go index a45d9e4e..e8ebe08d 100644 --- a/hrp/testcase.go +++ b/hrp/testcase.go @@ -257,6 +257,10 @@ func (tc *TestCase) loadISteps() (*TestCase, error) { testCase.TestSteps = append(testCase.TestSteps, &StepMobile{ step: step, }) + } else if step.Harmony != nil { + testCase.TestSteps = append(testCase.TestSteps, &StepMobile{ + step: step, + }) } else if step.Android != nil { testCase.TestSteps = append(testCase.TestSteps, &StepMobile{ step: step,