349 lines
8.8 KiB
Go
349 lines
8.8 KiB
Go
package uixt
|
|
|
|
import (
|
|
"crypto/md5"
|
|
"fmt"
|
|
"io"
|
|
"math"
|
|
"math/rand/v2"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/pkg/errors"
|
|
"github.com/rs/zerolog/log"
|
|
|
|
"github.com/httprunner/httprunner/v5/code"
|
|
"github.com/httprunner/httprunner/v5/internal/builtin"
|
|
"github.com/httprunner/httprunner/v5/internal/config"
|
|
"github.com/httprunner/httprunner/v5/uixt/option"
|
|
)
|
|
|
|
func convertToAbsoluteScope(driver IDriver, opts ...option.ActionOption) []option.ActionOption {
|
|
actionOptions := option.NewActionOptions(opts...)
|
|
|
|
// convert relative scope to absolute scope
|
|
if len(actionOptions.AbsScope) != 4 && len(actionOptions.Scope) == 4 {
|
|
scope := actionOptions.Scope
|
|
x1, y1, x2, y2, err := convertToAbsoluteCoordinates(
|
|
driver, scope[0], scope[1], scope[2], scope[3])
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("convert absolute scope failed")
|
|
return opts
|
|
}
|
|
actionOptions.AbsScope = []int{int(x1), int(y1), int(x2), int(y2)}
|
|
}
|
|
|
|
return actionOptions.Options()
|
|
}
|
|
|
|
func convertToAbsolutePoint(driver IDriver, x, y float64) (absX, absY float64, err error) {
|
|
// absolute coordinates
|
|
if x > 1 || y > 1 {
|
|
return x, y, nil
|
|
}
|
|
|
|
// relative coordinates
|
|
if assertRelative(x) && assertRelative(y) {
|
|
windowSize, err := driver.WindowSize()
|
|
if err != nil {
|
|
err = errors.Wrap(code.DeviceGetInfoError, err.Error())
|
|
return 0, 0, err
|
|
}
|
|
|
|
absX = math.Round(float64(windowSize.Width)*x*10) / 10
|
|
absY = math.Round(float64(windowSize.Height)*y*10) / 10
|
|
return absX, absY, nil
|
|
}
|
|
|
|
// invalid coordinates
|
|
err = errors.Wrap(code.InvalidCaseError,
|
|
fmt.Sprintf("invalid coordinates x(%f), y(%f)", x, y))
|
|
return
|
|
}
|
|
|
|
func convertToAbsoluteCoordinates(driver IDriver, fromX, fromY, toX, toY float64) (
|
|
absFromX, absFromY, absToX, absToY float64, err error) {
|
|
|
|
// absolute coordinates
|
|
if fromX > 1 || toX > 1 || fromY > 1 || toY > 1 {
|
|
return fromX, fromY, toX, toY, nil
|
|
}
|
|
|
|
// relative coordinates
|
|
if assertRelative(fromX) && assertRelative(fromY) &&
|
|
assertRelative(toX) && assertRelative(toY) {
|
|
windowSize, err := driver.WindowSize()
|
|
if err != nil {
|
|
err = errors.Wrap(code.DeviceGetInfoError, err.Error())
|
|
return 0, 0, 0, 0, err
|
|
}
|
|
width := windowSize.Width
|
|
height := windowSize.Height
|
|
|
|
absFromX = float64(width) * fromX
|
|
absFromY = float64(height) * fromY
|
|
absToX = float64(width) * toX
|
|
absToY = float64(height) * toY
|
|
|
|
return absFromX, absFromY, absToX, absToY, nil
|
|
}
|
|
|
|
// invalid coordinates
|
|
err = errors.Wrap(code.InvalidCaseError,
|
|
fmt.Sprintf("invalid coordinates fromX(%f), fromY(%f), toX(%f), toY(%f)",
|
|
fromX, fromY, toX, toY))
|
|
return
|
|
}
|
|
|
|
func assertRelative(p float64) bool {
|
|
return p >= 0 && p <= 1
|
|
}
|
|
|
|
func (dExt *XTDriver) Setup() error {
|
|
// unlock device screen
|
|
err := dExt.Unlock()
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("unlock device screen failed")
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (dExt *XTDriver) GetData(withReset bool) map[string]interface{} {
|
|
session := dExt.GetSession()
|
|
data := map[string]interface{}{
|
|
"requests": session.History(),
|
|
"screen_results": dExt.screenResults,
|
|
}
|
|
if withReset {
|
|
session.Reset()
|
|
dExt.screenResults = make([]*ScreenResult, 0)
|
|
}
|
|
return data
|
|
}
|
|
|
|
func (dExt *XTDriver) assertOCR(text, assert string) error {
|
|
var opts []option.ActionOption
|
|
opts = append(opts, option.WithScreenShotFileName(fmt.Sprintf("assert_ocr_%s", text)))
|
|
|
|
switch assert {
|
|
case AssertionEqual:
|
|
_, err := dExt.FindScreenText(text, opts...)
|
|
if err != nil {
|
|
return errors.Wrap(err, "assert ocr equal failed")
|
|
}
|
|
case AssertionNotEqual:
|
|
_, err := dExt.FindScreenText(text, opts...)
|
|
if err == nil {
|
|
return errors.New("assert ocr not equal failed")
|
|
}
|
|
case AssertionExists:
|
|
opts = append(opts, option.WithRegex(true))
|
|
_, err := dExt.FindScreenText(text, opts...)
|
|
if err != nil {
|
|
return errors.Wrap(err, "assert ocr exists failed")
|
|
}
|
|
case AssertionNotExists:
|
|
opts = append(opts, option.WithRegex(true))
|
|
_, err := dExt.FindScreenText(text, opts...)
|
|
if err == nil {
|
|
return errors.New("assert ocr not exists failed")
|
|
}
|
|
default:
|
|
return fmt.Errorf("unexpected assert method %s", assert)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (dExt *XTDriver) assertForegroundApp(appName, assert string) error {
|
|
app, err := dExt.ForegroundInfo()
|
|
if err != nil {
|
|
log.Warn().Err(err).Msg("get foreground app failed, skip app assertion")
|
|
return nil // Notice: ignore error when get foreground app failed
|
|
}
|
|
|
|
switch assert {
|
|
case AssertionEqual:
|
|
if app.PackageName != appName {
|
|
return errors.Wrap(err, "assert foreground app equal failed")
|
|
}
|
|
case AssertionNotEqual:
|
|
if app.PackageName == appName {
|
|
return errors.New("assert foreground app not equal failed")
|
|
}
|
|
default:
|
|
return fmt.Errorf("unexpected assert method %s", assert)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (dExt *XTDriver) DoValidation(check, assert, expected string, message ...string) (err error) {
|
|
switch check {
|
|
case SelectorOCR:
|
|
err = dExt.assertOCR(expected, assert)
|
|
// case SelectorAI:
|
|
// // TODO
|
|
case SelectorForegroundApp:
|
|
err = dExt.assertForegroundApp(expected, assert)
|
|
default:
|
|
return fmt.Errorf("validator %s not implemented", check)
|
|
}
|
|
|
|
if err != nil {
|
|
if message == nil {
|
|
message = []string{""}
|
|
}
|
|
log.Error().Err(err).Str("assert", assert).Str("expect", expected).
|
|
Str("msg", message[0]).Msg("validate failed")
|
|
return err
|
|
}
|
|
|
|
log.Info().Str("check", check).Str("assert", assert).
|
|
Str("expect", expected).Msg("validate success")
|
|
return nil
|
|
}
|
|
|
|
type SleepConfig struct {
|
|
StartTime time.Time `json:"start_time"`
|
|
Seconds float64 `json:"seconds,omitempty"`
|
|
Milliseconds int64 `json:"milliseconds,omitempty"`
|
|
}
|
|
|
|
// getSimulationDuration returns simulation duration by given params (in seconds)
|
|
func getSimulationDuration(params []float64) (milliseconds int64) {
|
|
if len(params) == 1 {
|
|
// given constant duration time
|
|
return int64(params[0] * 1000)
|
|
}
|
|
|
|
if len(params) == 2 {
|
|
// given [min, max], missing weight
|
|
// append default weight 1
|
|
params = append(params, 1.0)
|
|
}
|
|
|
|
var sections []struct {
|
|
min, max, weight float64
|
|
}
|
|
totalProb := 0.0
|
|
for i := 0; i+3 <= len(params); i += 3 {
|
|
min := params[i]
|
|
max := params[i+1]
|
|
weight := params[i+2]
|
|
totalProb += weight
|
|
sections = append(sections,
|
|
struct{ min, max, weight float64 }{min, max, weight},
|
|
)
|
|
}
|
|
|
|
if totalProb == 0 {
|
|
log.Warn().Msg("total weight is 0, skip simulation")
|
|
return 0
|
|
}
|
|
|
|
r := rand.Float64()
|
|
accProb := 0.0
|
|
for _, s := range sections {
|
|
accProb += s.weight / totalProb
|
|
if r < accProb {
|
|
milliseconds := int64((s.min + rand.Float64()*(s.max-s.min)) * 1000)
|
|
log.Info().Int64("random(ms)", milliseconds).
|
|
Interface("strategy_params", params).Msg("get simulation duration")
|
|
return milliseconds
|
|
}
|
|
}
|
|
|
|
log.Warn().Interface("strategy_params", params).
|
|
Msg("get simulation duration failed, skip simulation")
|
|
return 0
|
|
}
|
|
|
|
// sleepStrict sleeps strict duration with given params
|
|
// startTime is used to correct sleep duration caused by process time
|
|
func sleepStrict(startTime time.Time, strictMilliseconds int64) {
|
|
var elapsed int64
|
|
if !startTime.IsZero() {
|
|
elapsed = time.Since(startTime).Milliseconds()
|
|
}
|
|
dur := strictMilliseconds - elapsed
|
|
|
|
// if elapsed time is greater than given duration, skip sleep to reduce deviation caused by process time
|
|
if dur <= 0 {
|
|
log.Warn().
|
|
Int64("elapsed(ms)", elapsed).
|
|
Int64("strictSleep(ms)", strictMilliseconds).
|
|
Msg("elapsed >= simulation duration, skip sleep")
|
|
return
|
|
}
|
|
|
|
log.Info().Int64("sleepDuration(ms)", dur).
|
|
Int64("elapsed(ms)", elapsed).
|
|
Int64("strictSleep(ms)", strictMilliseconds).
|
|
Msg("sleep remaining duration time")
|
|
time.Sleep(time.Duration(dur) * time.Millisecond)
|
|
}
|
|
|
|
// global file lock
|
|
var (
|
|
fileLocks sync.Map
|
|
)
|
|
|
|
func DownloadFileByUrl(fileUrl string) (filePath string, err error) {
|
|
hash := md5.Sum([]byte(fileUrl))
|
|
fileName := fmt.Sprintf("%x", hash)
|
|
filePath = filepath.Join(config.GetConfig().DownloadsPath, fileName)
|
|
|
|
// get or create file lock
|
|
lockI, _ := fileLocks.LoadOrStore(filePath, &sync.Mutex{})
|
|
lock := lockI.(*sync.Mutex)
|
|
lock.Lock()
|
|
defer lock.Unlock()
|
|
|
|
if builtin.FileExists(filePath) {
|
|
return filePath, nil
|
|
}
|
|
|
|
log.Info().Str("fileUrl", fileUrl).Str("filePath", filePath).Msg("downloading file")
|
|
|
|
// Create an HTTP client with default settings.
|
|
client := &http.Client{}
|
|
|
|
// Build the HTTP GET request.
|
|
req, err := http.NewRequest("GET", fileUrl, nil)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// Perform the request.
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
// Check the HTTP status code.
|
|
if resp.StatusCode != http.StatusOK {
|
|
return "", fmt.Errorf("failed to download file: %s", resp.Status)
|
|
}
|
|
|
|
// Create the output file.
|
|
outFile, err := os.Create(filePath)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer outFile.Close()
|
|
|
|
// Copy the response body to the file.
|
|
_, err = io.Copy(outFile, resp.Body)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
log.Info().Str("filePath", filePath).Msg("download file success")
|
|
return filePath, nil
|
|
}
|