diff --git a/examples/uitest/demo_weixin_live.json b/examples/uitest/demo_weixin_live.json index 9aa990f3..f5f3f23a 100644 --- a/examples/uitest/demo_weixin_live.json +++ b/examples/uitest/demo_weixin_live.json @@ -1,6 +1,13 @@ { "config": { - "name": "通过 feed 卡片进入微信直播间" + "name": "通过 feed 卡片进入微信直播间", + "ios": [ + { + "port": 8700, + "mjpeg_port": 8800, + "log_on": true + } + ] }, "teststeps": [ { @@ -40,7 +47,8 @@ }, { "method": "tap_ocr", - "params": "视频号" + "params": "视频号", + "identifier": "进入视频号" } ] } diff --git a/examples/uitest/demo_weixin_live.yaml b/examples/uitest/demo_weixin_live.yaml index 45393eb2..44b19b50 100644 --- a/examples/uitest/demo_weixin_live.yaml +++ b/examples/uitest/demo_weixin_live.yaml @@ -1,5 +1,9 @@ config: name: 通过 feed 卡片进入微信直播间 + ios: + - port: 8700 + mjpeg_port: 8800 + log_on: true teststeps: - name: 启动微信 ios: @@ -22,6 +26,7 @@ teststeps: params: 发现 - method: tap_ocr params: 视频号 + identifier: 进入视频号 - name: 处理青少年弹窗 ios: actions: diff --git a/examples/uitest/demo_weixin_test.go b/examples/uitest/demo_weixin_test.go index 1031aefa..c692ddf1 100644 --- a/examples/uitest/demo_weixin_test.go +++ b/examples/uitest/demo_weixin_test.go @@ -9,7 +9,8 @@ import ( func TestIOSWeixinLive(t *testing.T) { testCase := &hrp.TestCase{ - Config: hrp.NewConfig("通过 feed 卡片进入微信直播间"), // .SetIOS(hrp.WDADevice{Port: 8700, MjpegPort: 8800}) + Config: hrp.NewConfig("通过 feed 卡片进入微信直播间"). + SetIOS(hrp.WithLogOn(true), hrp.WithPort(8700), hrp.WithMjpegPort(8800)), TestSteps: []hrp.IStep{ hrp.NewStep("启动微信"). IOS(). @@ -20,8 +21,8 @@ func TestIOSWeixinLive(t *testing.T) { AssertLabelExists("通讯录", "微信启动失败,「通讯录」不存在"), hrp.NewStep("进入直播页"). IOS(). - Tap("发现"). // 进入「发现页」 - TapByOCR("视频号"), // 通过 OCR 识别「视频号」 + Tap("发现"). // 进入「发现页」 + TapByOCR("视频号", hrp.WithIdentifier("进入视频号")), // 通过 OCR 识别「视频号」 hrp.NewStep("处理青少年弹窗"). IOS(). TapByOCR("我知道了", hrp.WithIgnoreNotFoundError(true)), diff --git a/hrp/config.go b/hrp/config.go index 103ea986..1884368b 100644 --- a/hrp/config.go +++ b/hrp/config.go @@ -29,7 +29,7 @@ type TConfig struct { ParametersSetting *TParamsConfig `json:"parameters_setting,omitempty" yaml:"parameters_setting,omitempty"` ThinkTimeSetting *ThinkTimeConfig `json:"think_time,omitempty" yaml:"think_time,omitempty"` WebSocketSetting *WebSocketConfig `json:"websocket,omitempty" yaml:"websocket,omitempty"` - IOS []*IOSConfig `json:"ios,omitempty" yaml:"ios,omitempty"` + IOS []*WDAOptions `json:"ios,omitempty" yaml:"ios,omitempty"` Timeout float64 `json:"timeout,omitempty" yaml:"timeout,omitempty"` // global timeout in seconds Export []string `json:"export,omitempty" yaml:"export,omitempty"` Weight int `json:"weight,omitempty" yaml:"weight,omitempty"` @@ -100,23 +100,23 @@ func (c *TConfig) SetWebSocket(times, interval, timeout, size int64) *TConfig { return c } -func (c *TConfig) SetIOS(device WDADevice) *TConfig { +func (c *TConfig) SetIOS(options ...WDAOption) *TConfig { + wdaOptions := &WDAOptions{} + for _, option := range options { + option(wdaOptions) + } + // each device can have its own settings - if device.UDID != "" { - c.IOS = append(c.IOS, &IOSConfig{ - WDADevice: device, - }) + if wdaOptions.UDID != "" { + c.IOS = append(c.IOS, wdaOptions) return c } - // device UDID is not specified ,settings will be shared - iosConfig := &IOSConfig{ - WDADevice: device, - } + // device UDID is not specified, settings will be shared if len(c.IOS) == 0 { - c.IOS = append(c.IOS, iosConfig) + c.IOS = append(c.IOS, wdaOptions) } else { - c.IOS[0] = iosConfig + c.IOS[0] = wdaOptions } return c } diff --git a/hrp/internal/dial/dns.go b/hrp/internal/dial/dns.go index 7f1e7e03..20a3f3d4 100644 --- a/hrp/internal/dial/dns.go +++ b/hrp/internal/dial/dns.go @@ -1,7 +1,6 @@ package dial import ( - "encoding/json" "fmt" "io/ioutil" "net" @@ -18,6 +17,7 @@ import ( "github.com/rs/zerolog/log" "github.com/httprunner/httprunner/v4/hrp/internal/builtin" + "github.com/httprunner/httprunner/v4/hrp/internal/json" ) const ( diff --git a/hrp/internal/uixt/drag_test.go b/hrp/internal/uixt/drag_test.go index 09b554b1..258c515c 100644 --- a/hrp/internal/uixt/drag_test.go +++ b/hrp/internal/uixt/drag_test.go @@ -5,7 +5,7 @@ import ( ) func TestDriverExt_Drag(t *testing.T) { - driverExt, err := InitWDAClient() + driverExt, err := InitWDAClient(nil) checkErr(t, err) pathSearch := "/Users/hero/Documents/temp/2020-05/opencv/IMG_map.png" diff --git a/hrp/internal/uixt/ext.go b/hrp/internal/uixt/ext.go index c140edc2..d27d3815 100644 --- a/hrp/internal/uixt/ext.go +++ b/hrp/internal/uixt/ext.go @@ -46,6 +46,7 @@ type DriverExt struct { frame *bytes.Buffer doneMjpegStream chan bool scale float64 + host string CVArgs } diff --git a/hrp/internal/uixt/init.go b/hrp/internal/uixt/init.go index 7f45db42..4c6b39e7 100644 --- a/hrp/internal/uixt/init.go +++ b/hrp/internal/uixt/init.go @@ -1,9 +1,16 @@ package uixt import ( + "bytes" + "fmt" + "io/ioutil" + "net/http" + "github.com/electricbubble/gwda" "github.com/pkg/errors" "github.com/rs/zerolog/log" + + "github.com/httprunner/httprunner/v4/hrp/internal/json" ) const ( @@ -21,9 +28,53 @@ const ( dismissAlertButtonSelector = "**/XCUIElementTypeButton[`label IN {'不允许','暂不'}`]" ) -func InitWDAClient(options ...gwda.DeviceOption) (*DriverExt, error) { +type WDAOptions struct { + UDID string `json:"udid,omitempty" yaml:"udid,omitempty"` + Port int `json:"port,omitempty" yaml:"port,omitempty"` + MjpegPort int `json:"mjpeg_port,omitempty" yaml:"mjpeg_port,omitempty"` + LogOn bool `json:"log_on,omitempty" yaml:"log_on,omitempty"` +} + +type WDAOption func(*WDAOptions) + +func WithUDID(udid string) WDAOption { + return func(device *WDAOptions) { + device.UDID = udid + } +} + +func WithPort(port int) WDAOption { + return func(device *WDAOptions) { + device.Port = port + } +} + +func WithMjpegPort(port int) WDAOption { + return func(device *WDAOptions) { + device.MjpegPort = port + } +} + +func WithLogOn(logOn bool) WDAOption { + return func(device *WDAOptions) { + device.LogOn = logOn + } +} + +func InitWDAClient(options *WDAOptions) (*DriverExt, error) { + var deviceOptions []gwda.DeviceOption + if options.UDID != "" { + deviceOptions = append(deviceOptions, gwda.WithSerialNumber(options.UDID)) + } + if options.Port != 0 { + deviceOptions = append(deviceOptions, gwda.WithPort(options.Port)) + } + if options.MjpegPort != 0 { + deviceOptions = append(deviceOptions, gwda.WithMjpegPort(options.MjpegPort)) + } + // init wda device - targetDevice, err := gwda.NewDevice(options...) + targetDevice, err := gwda.NewDevice(deviceOptions...) if err != nil { return nil, err } @@ -58,5 +109,71 @@ func InitWDAClient(options ...gwda.DeviceOption) (*DriverExt, error) { } log.Info().Interface("appiumWDASettings", settings).Msg("set appium WDA settings") + driverExt.host = fmt.Sprintf("http://127.0.0.1:%d", targetDevice.Port) + if options.LogOn { + err = driverExt.StartWDALog("hrp_wda_log") + if err != nil { + return nil, err + } + } + return driverExt, nil } + +type wdaResponse struct { + Value string `json:"value"` + SessionID string `json:"sessionId"` +} + +func (dExt *DriverExt) StartWDALog(identifier string) error { + log.Info().Msg("start WDA log recording") + data := map[string]interface{}{"action": "start", "type": 2, "identifier": identifier} + _, err := dExt.triggerWDALog(data) + if err != nil { + return errors.Wrap(err, "failed to start WDA log recording") + } + + return nil +} + +func (dExt *DriverExt) GetWDALog() (string, error) { + log.Info().Msg("stop WDA log recording") + data := map[string]interface{}{"action": "stop"} + reply, err := dExt.triggerWDALog(data) + if err != nil { + return "", errors.Wrap(err, "failed to get WDA logs") + } + + return reply.Value, nil +} + +func (dExt *DriverExt) triggerWDALog(data map[string]interface{}) (*wdaResponse, error) { + // [[FBRoute POST:@"/gtf/automation/log"].withoutSession respondWithTarget:self action:@selector(handleAutomationLog:)] + postJSON, err := json.Marshal(data) + if err != nil { + return nil, err + } + + url := fmt.Sprintf("%s/gtf/automation/log", dExt.host) + log.Info().Str("url", url).Interface("data", data).Msg("trigger WDA log") + resp, err := http.DefaultClient.Post(url, "application/json", bytes.NewBuffer(postJSON)) + if err != nil { + return nil, err + } + + if resp.StatusCode != http.StatusOK { + return nil, errors.Errorf("failed to trigger wda log, response status code: %d", resp.StatusCode) + } + + rawResp, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + reply := new(wdaResponse) + if err = json.Unmarshal(rawResp, reply); err != nil { + return nil, err + } + + return reply, nil +} diff --git a/hrp/internal/uixt/ocr_on.go b/hrp/internal/uixt/ocr_on.go index 79d0c271..68739172 100644 --- a/hrp/internal/uixt/ocr_on.go +++ b/hrp/internal/uixt/ocr_on.go @@ -5,7 +5,6 @@ package uixt import ( "bytes" "encoding/base64" - "encoding/json" "fmt" "image" "io/ioutil" @@ -13,6 +12,8 @@ import ( "net/http" "strings" "time" + + "github.com/httprunner/httprunner/v4/hrp/internal/json" ) var client = &http.Client{ diff --git a/hrp/internal/uixt/swipe_test.go b/hrp/internal/uixt/swipe_test.go index d7a1c8e9..9f4390d6 100644 --- a/hrp/internal/uixt/swipe_test.go +++ b/hrp/internal/uixt/swipe_test.go @@ -5,7 +5,7 @@ import ( ) func TestSwipeUntil(t *testing.T) { - driverExt, err := InitWDAClient() + driverExt, err := InitWDAClient(nil) checkErr(t, err) var x, y, width, height float64 diff --git a/hrp/internal/uixt/tap_test.go b/hrp/internal/uixt/tap_test.go index 9c8c2e09..482c06f4 100644 --- a/hrp/internal/uixt/tap_test.go +++ b/hrp/internal/uixt/tap_test.go @@ -5,7 +5,7 @@ import ( ) func TestDriverExt_TapWithNumber(t *testing.T) { - driverExt, err := InitWDAClient() + driverExt, err := InitWDAClient(nil) checkErr(t, err) pathSearch := "/Users/hero/Documents/temp/2020-05/opencv/flag7.png" @@ -20,7 +20,7 @@ func TestDriverExt_TapWithNumber(t *testing.T) { } func TestDriverExt_TapXY(t *testing.T) { - driverExt, err := InitWDAClient() + driverExt, err := InitWDAClient(nil) checkErr(t, err) err = driverExt.TapXY(0.4, 0.5, "") @@ -28,7 +28,7 @@ func TestDriverExt_TapXY(t *testing.T) { } func TestDriverExt_TapWithOCR(t *testing.T) { - driverExt, err := InitWDAClient() + driverExt, err := InitWDAClient(nil) checkErr(t, err) // 需要点击文字上方的图标 diff --git a/hrp/internal/uixt/touch_test.go b/hrp/internal/uixt/touch_test.go index a814b12b..9ec38aee 100644 --- a/hrp/internal/uixt/touch_test.go +++ b/hrp/internal/uixt/touch_test.go @@ -5,7 +5,7 @@ import ( ) func TestDriverExt_ForceTouch(t *testing.T) { - driverExt, err := InitWDAClient() + driverExt, err := InitWDAClient(nil) checkErr(t, err) pathSearch := "/Users/hero/Documents/temp/2020-05/opencv/IMG_ft.png" @@ -21,7 +21,7 @@ func TestDriverExt_ForceTouch(t *testing.T) { } func TestDriverExt_TouchAndHold(t *testing.T) { - driverExt, err := InitWDAClient() + driverExt, err := InitWDAClient(nil) checkErr(t, err) pathSearch := "/Users/hero/Documents/temp/2020-05/opencv/IMG_ft.png" diff --git a/hrp/runner.go b/hrp/runner.go index 690fc232..a34783fa 100644 --- a/hrp/runner.go +++ b/hrp/runner.go @@ -386,7 +386,7 @@ func (r *testCaseRunner) parseConfig() error { // init iOS WDA clients for _, iosDeviceConfig := range r.parsedConfig.IOS { - _, err := r.hrpRunner.InitWDAClient(iosDeviceConfig.WDADevice) + _, err := r.hrpRunner.InitWDAClient(iosDeviceConfig) if err != nil { return errors.Wrap(err, "init iOS WDA client failed") } diff --git a/hrp/session.go b/hrp/session.go index 35192bf4..359981b7 100644 --- a/hrp/session.go +++ b/hrp/session.go @@ -7,6 +7,8 @@ import ( "github.com/gorilla/websocket" "github.com/pkg/errors" "github.com/rs/zerolog/log" + + "github.com/httprunner/httprunner/v4/hrp/internal/json" ) // SessionRunner is used to run testcase and its steps. @@ -160,5 +162,20 @@ func (r *SessionRunner) GetSummary() *TestCaseSummary { } caseSummary.InOut.ExportVars = exportVars caseSummary.InOut.ConfigVars = r.parsedConfig.Variables + + logs := make(map[string]string) + for udid, client := range r.hrpRunner.wdaClients { + log, err := client.GetWDALog() + if err != nil { + logs[udid] = err.Error() + } else { + logs[udid] = log + } + + } + logsStr, _ := json.Marshal(logs) + caseSummary.Logs = string(logsStr) + + // caseSummary.Log return caseSummary } diff --git a/hrp/step_ios_ui.go b/hrp/step_ios_ui.go index 43997f05..836e1049 100644 --- a/hrp/step_ios_ui.go +++ b/hrp/step_ios_ui.go @@ -6,25 +6,26 @@ import ( "strings" "time" - "github.com/electricbubble/gwda" "github.com/pkg/errors" "github.com/rs/zerolog/log" "github.com/httprunner/httprunner/v4/hrp/internal/uixt" ) -type IOSConfig struct { - WDADevice -} +type ( + WDAOptions = uixt.WDAOptions + WDAOption = uixt.WDAOption +) -type WDADevice struct { - UDID string `json:"udid,omitempty" yaml:"udid,omitempty"` - Port int `json:"port,omitempty" yaml:"port,omitempty"` - MjpegPort int `json:"mjpeg_port,omitempty" yaml:"mjpeg_port,omitempty"` -} +var ( + WithUDID = uixt.WithUDID + WithPort = uixt.WithPort + WithMjpegPort = uixt.WithMjpegPort + WithLogOn = uixt.WithLogOn +) type IOSStep struct { - WDADevice `yaml:",inline"` // inline refers to https://pkg.go.dev/gopkg.in/yaml.v3#Marshal + WDAOptions `yaml:",inline"` // inline refers to https://pkg.go.dev/gopkg.in/yaml.v3#Marshal MobileAction `yaml:",inline"` Actions []MobileAction `json:"actions,omitempty" yaml:"actions,omitempty"` } @@ -462,36 +463,26 @@ func (s *StepIOSValidation) Run(r *SessionRunner) (*StepResult, error) { return runStepIOS(r, s.step) } -func (r *HRPRunner) InitWDAClient(device WDADevice) (client *uiDriver, err error) { +func (r *HRPRunner) InitWDAClient(options *WDAOptions) (client *uiDriver, err error) { // avoid duplicate init - if device.UDID == "" && len(r.wdaClients) == 1 { + if options.UDID == "" && len(r.wdaClients) == 1 { for _, v := range r.wdaClients { return v, nil } } // avoid duplicate init - if device.UDID != "" { - if client, ok := r.wdaClients[device.UDID]; ok { + if options.UDID != "" { + if client, ok := r.wdaClients[options.UDID]; ok { return client, nil } } - var deviceOptions []gwda.DeviceOption - if device.UDID != "" { - deviceOptions = append(deviceOptions, gwda.WithSerialNumber(device.UDID)) - } - if device.Port != 0 { - deviceOptions = append(deviceOptions, gwda.WithPort(device.Port)) - } - if device.MjpegPort != 0 { - deviceOptions = append(deviceOptions, gwda.WithMjpegPort(device.MjpegPort)) - } - - driverExt, err := uixt.InitWDAClient(deviceOptions...) + driverExt, err := uixt.InitWDAClient(options) if err != nil { return nil, err } + client = &uiDriver{ DriverExt: *driverExt, } @@ -500,7 +491,7 @@ func (r *HRPRunner) InitWDAClient(device WDADevice) (client *uiDriver, err error if r.wdaClients == nil { r.wdaClients = make(map[string]*uiDriver) } - r.wdaClients[device.UDID] = client + r.wdaClients[options.UDID] = client return client, nil } @@ -515,7 +506,7 @@ func runStepIOS(s *SessionRunner, step *TStep) (stepResult *StepResult, err erro screenshots := make([]string, 0) // init wdaClient driver - wdaClient, err := s.hrpRunner.InitWDAClient(step.IOS.WDADevice) + wdaClient, err := s.hrpRunner.InitWDAClient(&step.IOS.WDAOptions) if err != nil { return } diff --git a/hrp/step_ios_ui_test.go b/hrp/step_ios_ui_test.go index 9f867a22..3de1f6db 100644 --- a/hrp/step_ios_ui_test.go +++ b/hrp/step_ios_ui_test.go @@ -50,7 +50,7 @@ func TestIOSSearchApp(t *testing.T) { func TestIOSAppLaunch(t *testing.T) { testCase := &TestCase{ Config: NewConfig("启动 & 关闭 App"). - SetIOS(WDADevice{Port: 8100, MjpegPort: 9100}), + SetIOS(WithPort(8100), WithMjpegPort(9100)), TestSteps: []IStep{ NewStep("终止今日头条"). IOS().AppTerminate("com.ss.iphone.article.News"), @@ -73,7 +73,7 @@ func TestIOSAppLaunch(t *testing.T) { func TestIOSWeixinLive(t *testing.T) { testCase := &TestCase{ Config: NewConfig("ios ui action on 微信直播"). - SetIOS(WDADevice{Port: 8100, MjpegPort: 9100}), + SetIOS(WithLogOn(true), WithPort(8100), WithMjpegPort(9100)), TestSteps: []IStep{ NewStep("启动微信"). IOS(). diff --git a/hrp/summary.go b/hrp/summary.go index b73522cc..a944d6a3 100644 --- a/hrp/summary.go +++ b/hrp/summary.go @@ -151,7 +151,7 @@ type TestCaseSummary struct { Stat *TestStepStat `json:"stat" yaml:"stat"` Time *TestCaseTime `json:"time" yaml:"time"` InOut *TestCaseInOut `json:"in_out" yaml:"in_out"` - Log string `json:"log,omitempty" yaml:"log,omitempty"` // TODO + Logs string `json:"logs,omitempty" yaml:"logs,omitempty"` // TODO Records []*StepResult `json:"records" yaml:"records"` RootDir string `json:"root_dir" yaml:"root_dir"` }