mirror of
https://github.com/httprunner/httprunner.git
synced 2026-09-06 16:07:45 +08:00
feat: scenario detect of mobile ui automation
This commit is contained in:
@@ -325,3 +325,21 @@ func (dExt *DriverExt) FindTextsByOCR(ocrTexts []string, options ...DataOption)
|
|||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SDService interface {
|
||||||
|
SceneDetection(detectImage []byte, detectType string) (bool, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dExt *DriverExt) ScenarioDetect(scenarioType string, options ...DataOption) (res bool, err error) {
|
||||||
|
var bufSource *bytes.Buffer
|
||||||
|
if bufSource, err = dExt.takeScreenShot(); err != nil {
|
||||||
|
err = fmt.Errorf("takeScreenShot error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
service, err := newVEDEMSDService()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return service.SceneDetection(bufSource.Bytes(), scenarioType)
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
//go:build localtest
|
||||||
|
|
||||||
|
package uixt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func checkCP(buff []byte) error {
|
||||||
|
service, err := newVEDEMCPService()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
sdResults, err := service.getCPResult(buff)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Println(sdResults)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCPWithScreenshot(t *testing.T) {
|
||||||
|
device, _ := NewIOSDevice(WithWDAPort(8700), WithWDAMjpegPort(8800))
|
||||||
|
driver, err := device.NewUSBDriver(nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
raw, err := driver.Screenshot()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := checkCP(raw.Bytes()); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCPWithLocalFile(t *testing.T) {
|
||||||
|
imagePath := "~/Downloads/1669385239_validate_1669385367.png"
|
||||||
|
file, err := os.ReadFile(imagePath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := checkCP(file); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -47,6 +47,8 @@ const (
|
|||||||
SelectorOCR string = "ui_ocr"
|
SelectorOCR string = "ui_ocr"
|
||||||
SelectorImage string = "ui_image"
|
SelectorImage string = "ui_image"
|
||||||
SelectorForegroundApp string = "ui_foreground_app"
|
SelectorForegroundApp string = "ui_foreground_app"
|
||||||
|
ScenarioType string = "scenario_type"
|
||||||
|
|
||||||
// assertions
|
// assertions
|
||||||
AssertionEqual string = "equal"
|
AssertionEqual string = "equal"
|
||||||
AssertionNotEqual string = "not_equal"
|
AssertionNotEqual string = "not_equal"
|
||||||
@@ -390,6 +392,11 @@ func (dExt *DriverExt) IsAppInForeground(packageName string) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (dExt *DriverExt) IsScenarioType(scenarioType string) bool {
|
||||||
|
_, _, _, _, err := dExt.FindImageRectInUIKit(scenarioType)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
var errActionNotImplemented = errors.New("UI action not implemented")
|
var errActionNotImplemented = errors.New("UI action not implemented")
|
||||||
|
|
||||||
func convertToFloat64(val interface{}) (float64, error) {
|
func convertToFloat64(val interface{}) (float64, error) {
|
||||||
@@ -749,6 +756,8 @@ func (dExt *DriverExt) DoValidation(check, assert, expected string, message ...s
|
|||||||
result = (dExt.IsImageExist(expected) == exp)
|
result = (dExt.IsImageExist(expected) == exp)
|
||||||
case SelectorForegroundApp:
|
case SelectorForegroundApp:
|
||||||
result = (dExt.IsAppInForeground(expected) == exp)
|
result = (dExt.IsAppInForeground(expected) == exp)
|
||||||
|
case ScenarioType:
|
||||||
|
result, _ = dExt.ScenarioDetect(expected)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !result {
|
if !result {
|
||||||
|
|||||||
+45
-87
@@ -3,7 +3,6 @@ package uixt
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"image"
|
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -17,15 +16,42 @@ import (
|
|||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
CHECK_LIVE_TYPE_GAME = "checkLiveTypeGame" // 直播游戏
|
||||||
|
CHECK_LIVE_TYPE_SHOW = "checkLiveTypeShow" // 直播秀场
|
||||||
|
CHECK_LIVE_TYPE_PEOPLE = "checkLiveTypePeople" // 直播多人
|
||||||
|
CHECK_LIVE_TYPE_SHOP = "checkLiveTypeShop" // 直播电商
|
||||||
|
CHECK_BLACK_OR_WHITE = "checkBlackOrWhite" // 黑白屏
|
||||||
|
CHECK_PART_BLACK_OR_WHITE = "checkPartBlackOrWhite" // 部分黑白屏
|
||||||
|
CHECK_HALF_BLANK = "checkHalfBlank" // 半白屏检测
|
||||||
|
CHECK_PAGE_ERROR = "checkPageError" // 页面乱码
|
||||||
|
CHECK_SNOW = "checkSnow" // 雪花屏
|
||||||
|
CHECK_OVER_LAY = "checkOverlay" // 图像重叠
|
||||||
|
CHECK_LOAD_FAILED = "checkLoadFailed" // 图像加载失败检测
|
||||||
|
CHECK_DETECT_COLOR_BLOCK = "detectColorBlock" // 游戏色块检测
|
||||||
|
CHECK_PURPLE = "checkPurple" // 游戏紫块
|
||||||
|
CHECK_WHITE_RECT = "checkWhiteRect" // 游戏白块
|
||||||
|
CHECK_CORRUPT = "checkCorrupt" // 游戏花屏
|
||||||
|
CHECK_BLACK_EDGE = "checkBlackEdge" // 游戏黑边
|
||||||
|
CHECK_WHITE_RATIO = "checkWhiteRatio" // 白屏占比
|
||||||
|
CHECK_OVER_EXPOSURE = "checkOverExposure" // 图像过爆
|
||||||
|
CHECK_DIALOG = "checkDialog" // 弹窗检测
|
||||||
|
CHECK_TEXT_OVER_LAP = "checkTextOverlap" // 文字重叠
|
||||||
|
CHECK_TEXT_OVER_STEP = "checkTextOverstep" // 文字超框
|
||||||
|
CHECK_VIDEO_CORRUPT = "checkVideoCorrupt" // 视频花屏
|
||||||
|
CHECK_GREEN_VIDEO = "checkGreenVideo" // 视频绿屏
|
||||||
|
CHECK_DEFAULTCHECKED = "checkDefaultChecked" // 合规检测
|
||||||
|
)
|
||||||
|
|
||||||
type SDResult struct {
|
type SDResult struct {
|
||||||
Image string `json:"image"`
|
Image string `json:"image"`
|
||||||
Points []PointF `json:"points"`
|
Points []PointF `json:"points"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SDResponse struct {
|
type SDResponse struct {
|
||||||
Code int `json:"code"`
|
Code int `json:"code"`
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
Result []SDResult `json:"result"`
|
Result bool `json:"result"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type veDEMSDService struct{}
|
type veDEMSDService struct{}
|
||||||
@@ -50,43 +76,33 @@ func checkSDEnv() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *veDEMSDService) getSDResult(searchImage []byte, sourceImage []byte) ([]SDResult, error) {
|
func (s *veDEMSDService) SceneDetection(detectImage []byte, detectType string) (bool, error) {
|
||||||
bodyBuf := &bytes.Buffer{}
|
bodyBuf := &bytes.Buffer{}
|
||||||
bodyWriter := multipart.NewWriter(bodyBuf)
|
bodyWriter := multipart.NewWriter(bodyBuf)
|
||||||
bodyWriter.WriteField("withDet", "true")
|
bodyWriter.WriteField("withDet", "true")
|
||||||
|
bodyWriter.WriteField("detectType", detectType)
|
||||||
// bodyWriter.WriteField("timestampOnly", "true")
|
// bodyWriter.WriteField("timestampOnly", "true")
|
||||||
|
|
||||||
formWriter, err := bodyWriter.CreateFormFile("searchImage", "searchImage.png")
|
formWriter, err := bodyWriter.CreateFormFile("image", "image.png")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(code.CVRequestError,
|
return false, errors.Wrap(code.CVRequestError,
|
||||||
fmt.Sprintf("create form file error: %v", err))
|
fmt.Sprintf("create form file error: %v", err))
|
||||||
}
|
}
|
||||||
size, err := formWriter.Write(searchImage)
|
size, err := formWriter.Write(detectImage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(code.CVRequestError,
|
return false, errors.Wrap(code.CVRequestError,
|
||||||
fmt.Sprintf("write form error: %v", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
formWriter, err = bodyWriter.CreateFormFile("sourceImage", "sourceImage.png")
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(code.CVRequestError,
|
|
||||||
fmt.Sprintf("create form file error: %v", err))
|
|
||||||
}
|
|
||||||
_, err = formWriter.Write(sourceImage)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(code.CVRequestError,
|
|
||||||
fmt.Sprintf("write form error: %v", err))
|
fmt.Sprintf("write form error: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
err = bodyWriter.Close()
|
err = bodyWriter.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(code.CVRequestError,
|
return false, errors.Wrap(code.CVRequestError,
|
||||||
fmt.Sprintf("close body writer error: %v", err))
|
fmt.Sprintf("close body writer error: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
req, err := http.NewRequest("POST", env.VEDEM_SD_URL, bodyBuf)
|
req, err := http.NewRequest("POST", env.VEDEM_SD_URL, bodyBuf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(code.CVRequestError,
|
return false, errors.Wrap(code.CVRequestError,
|
||||||
fmt.Sprintf("construct request error: %v", err))
|
fmt.Sprintf("construct request error: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,87 +129,29 @@ func (s *veDEMSDService) getSDResult(searchImage []byte, sourceImage []byte) ([]
|
|||||||
time.Sleep(1 * time.Second)
|
time.Sleep(1 * time.Second)
|
||||||
}
|
}
|
||||||
if resp == nil {
|
if resp == nil {
|
||||||
return nil, code.CVServiceConnectionError
|
return false, code.CVServiceConnectionError
|
||||||
}
|
}
|
||||||
|
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
results, err := ioutil.ReadAll(resp.Body)
|
results, err := ioutil.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(code.CVResponseError,
|
return false, errors.Wrap(code.CVResponseError,
|
||||||
fmt.Sprintf("read response body error: %v", err))
|
fmt.Sprintf("read response body error: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
return nil, errors.Wrap(code.CVResponseError,
|
return false, errors.Wrap(code.CVResponseError,
|
||||||
fmt.Sprintf("unexpected response status code: %d, results: %v",
|
fmt.Sprintf("unexpected response status code: %d, results: %v",
|
||||||
resp.StatusCode, string(results)))
|
resp.StatusCode, string(results)))
|
||||||
}
|
}
|
||||||
|
|
||||||
var cvResult SDResponse
|
var sdResult SDResponse
|
||||||
err = json.Unmarshal(results, &cvResult)
|
err = json.Unmarshal(results, &sdResult)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(code.CVResponseError,
|
return false, errors.Wrap(code.CVResponseError,
|
||||||
fmt.Sprintf("json unmarshal response body error: %v", err))
|
fmt.Sprintf("json unmarshal response body error: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
return cvResult.Result, nil
|
return sdResult.Result, nil
|
||||||
}
|
|
||||||
|
|
||||||
func (s *veDEMSDService) FindImage(byteSearch []byte, byteSource []byte, options ...DataOption) (rect image.Rectangle, err error) {
|
|
||||||
data := NewData(map[string]interface{}{}, options...)
|
|
||||||
|
|
||||||
cvResults, err := s.getSDResult(byteSearch, byteSource)
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msg("getSDResult failed")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var rects []image.Rectangle
|
|
||||||
var cvImages []string
|
|
||||||
for _, cvResult := range cvResults {
|
|
||||||
rect = image.Rectangle{
|
|
||||||
// cvResult.Points 顺序:左上 -> 右上 -> 右下 -> 左下
|
|
||||||
Min: image.Point{
|
|
||||||
X: int(cvResult.Points[0].X),
|
|
||||||
Y: int(cvResult.Points[0].Y),
|
|
||||||
},
|
|
||||||
Max: image.Point{
|
|
||||||
X: int(cvResult.Points[2].X),
|
|
||||||
Y: int(cvResult.Points[2].Y),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
if rect.Min.X >= data.Scope[0] && rect.Max.X <= data.Scope[2] && rect.Min.Y >= data.Scope[1] && rect.Max.Y <= data.Scope[3] {
|
|
||||||
cvImages = append(cvImages, cvResult.Image)
|
|
||||||
|
|
||||||
rects = append(rects, rect)
|
|
||||||
|
|
||||||
// match exactly, and not specify index, return the first one
|
|
||||||
if data.Index == 0 {
|
|
||||||
return rect, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(rects) == 0 {
|
|
||||||
return image.Rectangle{}, errors.Wrap(code.CVImageNotFoundError,
|
|
||||||
fmt.Sprintf("image not found"))
|
|
||||||
}
|
|
||||||
|
|
||||||
// get index
|
|
||||||
idx := data.Index
|
|
||||||
if idx > 0 {
|
|
||||||
// NOTICE: index start from 1
|
|
||||||
idx = idx - 1
|
|
||||||
} else if idx < 0 {
|
|
||||||
idx = len(rects) + idx
|
|
||||||
}
|
|
||||||
|
|
||||||
// index out of range
|
|
||||||
if idx >= len(rects) {
|
|
||||||
return image.Rectangle{}, errors.Wrap(code.CVImageNotFoundError,
|
|
||||||
fmt.Sprintf("image found, index %d out of range", idx))
|
|
||||||
}
|
|
||||||
|
|
||||||
return rects[idx], nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
//go:build localtest
|
||||||
|
|
||||||
|
package uixt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func checkSD(buff []byte, detectType string) error {
|
||||||
|
service, err := newVEDEMSDService()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
sdResults, err := service.SceneDetection(buff, detectType)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Println(sdResults)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSDWithScreenshot(t *testing.T) {
|
||||||
|
device, _ := NewIOSDevice(WithWDAPort(8700), WithWDAMjpegPort(8800))
|
||||||
|
driver, err := device.NewUSBDriver(nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
raw, err := driver.Screenshot()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := checkSD(raw.Bytes(), "checkLiveTypeShop"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSDWithLocalFile(t *testing.T) {
|
||||||
|
imagePath := "~/Downloads/s1.png"
|
||||||
|
file, err := os.ReadFile(imagePath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := checkSD(file, "checkLiveTypeShop"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
+9
-3
@@ -274,7 +274,7 @@ func (v *responseObject) searchRegexp(expr string) interface{} {
|
|||||||
return expr
|
return expr
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateUI(ud *uixt.DriverExt, iValidators []interface{}) (validateResults []*ValidationResult, err error) {
|
func validateUI(ud *uixt.DriverExt, iValidators []interface{}, parser *Parser, variablesMapping map[string]interface{}) (validateResults []*ValidationResult, err error) {
|
||||||
for _, iValidator := range iValidators {
|
for _, iValidator := range iValidators {
|
||||||
validator, ok := iValidator.(Validator)
|
validator, ok := iValidator.(Validator)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -287,14 +287,20 @@ func validateUI(ud *uixt.DriverExt, iValidators []interface{}) (validateResults
|
|||||||
}
|
}
|
||||||
|
|
||||||
// parse check value
|
// parse check value
|
||||||
if !strings.HasPrefix(validator.Check, "ui_") {
|
if !strings.HasPrefix(validator.Check, "ui_") && !strings.HasPrefix(validator.Check, "scenario") {
|
||||||
validataResult.CheckResult = "skip"
|
validataResult.CheckResult = "skip"
|
||||||
log.Warn().Interface("validator", validator).Msg("skip validator")
|
log.Warn().Interface("validator", validator).Msg("skip validator")
|
||||||
validateResults = append(validateResults, validataResult)
|
validateResults = append(validateResults, validataResult)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
expected, ok := validator.Expect.(string)
|
// parse expected value
|
||||||
|
expectValue, err := parser.Parse(validator.Expect, variablesMapping)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("validator expect should be string")
|
||||||
|
}
|
||||||
|
|
||||||
|
expected, ok := expectValue.(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, errors.New("validator expect should be string")
|
return nil, errors.New("validator expect should be string")
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-1
@@ -506,6 +506,21 @@ func (s *StepMobileUIValidation) AssertAppInForeground(packageName string, msg .
|
|||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *StepMobileUIValidation) AssertScenarioType(scenarioType string, msg ...string) *StepMobileUIValidation {
|
||||||
|
v := Validator{
|
||||||
|
Check: uixt.ScenarioType,
|
||||||
|
Assert: uixt.AssertionExists,
|
||||||
|
Expect: scenarioType,
|
||||||
|
}
|
||||||
|
if len(msg) > 0 {
|
||||||
|
v.Message = msg[0]
|
||||||
|
} else {
|
||||||
|
v.Message = fmt.Sprintf("cv image [%s] not found", scenarioType)
|
||||||
|
}
|
||||||
|
s.step.Validators = append(s.step.Validators, v)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
func (s *StepMobileUIValidation) AssertAppNotInForeground(packageName string, msg ...string) *StepMobileUIValidation {
|
func (s *StepMobileUIValidation) AssertAppNotInForeground(packageName string, msg ...string) *StepMobileUIValidation {
|
||||||
v := Validator{
|
v := Validator{
|
||||||
Check: uixt.SelectorForegroundApp,
|
Check: uixt.SelectorForegroundApp,
|
||||||
@@ -666,7 +681,7 @@ func runStepMobileUI(s *SessionRunner, step *TStep) (stepResult *StepResult, err
|
|||||||
}
|
}
|
||||||
|
|
||||||
// validate
|
// validate
|
||||||
validateResults, err := validateUI(uiDriver, step.Validators)
|
validateResults, err := validateUI(uiDriver, step.Validators, s.caseRunner.parser, stepVariables)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !code.IsErrorPredefined(err) {
|
if !code.IsErrorPredefined(err) {
|
||||||
err = errors.Wrap(code.MobileUIValidationError, err.Error())
|
err = errors.Wrap(code.MobileUIValidationError, err.Error())
|
||||||
|
|||||||
Reference in New Issue
Block a user