mirror of
https://github.com/httprunner/httprunner.git
synced 2026-05-15 04:19:28 +08:00
960 lines
24 KiB
Go
960 lines
24 KiB
Go
package gidevice
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/rs/zerolog/log"
|
|
uuid "github.com/satori/go.uuid"
|
|
"howett.net/plist"
|
|
|
|
"github.com/httprunner/httprunner/v4/hrp/pkg/gidevice/pkg/ipa"
|
|
"github.com/httprunner/httprunner/v4/hrp/pkg/gidevice/pkg/libimobiledevice"
|
|
"github.com/httprunner/httprunner/v4/hrp/pkg/gidevice/pkg/nskeyedarchiver"
|
|
)
|
|
|
|
const LockdownPort = 62078
|
|
|
|
var _ Device = (*device)(nil)
|
|
|
|
func newDevice(client *libimobiledevice.UsbmuxClient, properties DeviceProperties) *device {
|
|
return &device{
|
|
umClient: client,
|
|
properties: &properties,
|
|
}
|
|
}
|
|
|
|
type device struct {
|
|
umClient *libimobiledevice.UsbmuxClient
|
|
lockdownClient *libimobiledevice.LockdownClient
|
|
|
|
properties *DeviceProperties
|
|
|
|
lockdown *lockdown
|
|
imageMounter ImageMounter
|
|
screenshot Screenshot
|
|
simulateLocation SimulateLocation
|
|
installationProxy InstallationProxy
|
|
instruments Instruments
|
|
afc Afc
|
|
houseArrest HouseArrest
|
|
syslogRelay SyslogRelay
|
|
diagnosticsRelay DiagnosticsRelay
|
|
springBoard SpringBoard
|
|
crashReportMover CrashReportMover
|
|
pcapd Pcapd
|
|
perfd []Perfd
|
|
}
|
|
|
|
func (d *device) Properties() DeviceProperties {
|
|
return *d.properties
|
|
}
|
|
|
|
func (d *device) NewConnect(port int, timeout ...time.Duration) (InnerConn, error) {
|
|
newClient, err := libimobiledevice.NewUsbmuxClient(timeout...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var pkt libimobiledevice.Packet
|
|
if pkt, err = newClient.NewPlistPacket(
|
|
newClient.NewConnectRequest(d.properties.DeviceID, port),
|
|
); err != nil {
|
|
newClient.Close()
|
|
return nil, err
|
|
}
|
|
|
|
if err = newClient.SendPacket(pkt); err != nil {
|
|
newClient.Close()
|
|
return nil, err
|
|
}
|
|
|
|
if _, err = newClient.ReceivePacket(); err != nil {
|
|
newClient.Close()
|
|
return nil, err
|
|
}
|
|
|
|
return newClient.InnerConn(), err
|
|
}
|
|
|
|
func (d *device) ReadPairRecord() (pairRecord *PairRecord, err error) {
|
|
var pkt libimobiledevice.Packet
|
|
if pkt, err = d.umClient.NewPlistPacket(
|
|
d.umClient.NewReadPairRecordRequest(d.properties.SerialNumber),
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err = d.umClient.SendPacket(pkt); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var respPkt libimobiledevice.Packet
|
|
if respPkt, err = d.umClient.ReceivePacket(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
reply := struct {
|
|
Data []byte `plist:"PairRecordData"`
|
|
}{}
|
|
if err = respPkt.Unmarshal(&reply); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var record PairRecord
|
|
if _, err = plist.Unmarshal(reply.Data, &record); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
pairRecord = &record
|
|
return
|
|
}
|
|
|
|
func (d *device) SavePairRecord(pairRecord *PairRecord) (err error) {
|
|
var data []byte
|
|
if data, err = plist.Marshal(pairRecord, plist.XMLFormat); err != nil {
|
|
return err
|
|
}
|
|
|
|
var pkt libimobiledevice.Packet
|
|
if pkt, err = d.umClient.NewPlistPacket(
|
|
d.umClient.NewSavePairRecordRequest(d.properties.SerialNumber, d.properties.DeviceID, data),
|
|
); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err = d.umClient.SendPacket(pkt); err != nil {
|
|
return err
|
|
}
|
|
|
|
if _, err = d.umClient.ReceivePacket(); err != nil {
|
|
return err
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func (d *device) DeletePairRecord() (err error) {
|
|
var pkt libimobiledevice.Packet
|
|
if pkt, err = d.umClient.NewPlistPacket(
|
|
d.umClient.NewDeletePairRecordRequest(d.properties.SerialNumber),
|
|
); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err = d.umClient.SendPacket(pkt); err != nil {
|
|
return err
|
|
}
|
|
|
|
if _, err = d.umClient.ReceivePacket(); err != nil {
|
|
return err
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func (d *device) lockdownService() (lockdown Lockdown, err error) {
|
|
// if d.lockdown != nil {
|
|
// return d.lockdown, nil
|
|
// }
|
|
|
|
var innerConn InnerConn
|
|
if innerConn, err = d.NewConnect(LockdownPort, 0); err != nil {
|
|
return nil, err
|
|
}
|
|
d.lockdownClient = libimobiledevice.NewLockdownClient(innerConn)
|
|
d.lockdown = newLockdown(d)
|
|
_, err = d.lockdown._getProductVersion()
|
|
lockdown = d.lockdown
|
|
return
|
|
}
|
|
|
|
func (d *device) QueryType() (LockdownType, error) {
|
|
if _, err := d.lockdownService(); err != nil {
|
|
return LockdownType{}, err
|
|
}
|
|
return d.lockdown.QueryType()
|
|
}
|
|
|
|
func (d *device) GetValue(domain, key string) (v interface{}, err error) {
|
|
if _, err = d.lockdownService(); err != nil {
|
|
return nil, err
|
|
}
|
|
if d.lockdown.pairRecord == nil {
|
|
if err = d.lockdown.handshake(); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if err = d.lockdown.startSession(d.lockdown.pairRecord); err != nil {
|
|
return nil, err
|
|
}
|
|
if v, err = d.lockdown.GetValue(domain, key); err != nil {
|
|
return nil, err
|
|
}
|
|
err = d.lockdown.stopSession()
|
|
return
|
|
}
|
|
|
|
func (d *device) Pair() (pairRecord *PairRecord, err error) {
|
|
if _, err = d.lockdownService(); err != nil {
|
|
return nil, err
|
|
}
|
|
return d.lockdown.Pair()
|
|
}
|
|
|
|
func (d *device) imageMounterService() (imageMounter ImageMounter, err error) {
|
|
if d.imageMounter != nil {
|
|
return d.imageMounter, nil
|
|
}
|
|
if _, err = d.lockdownService(); err != nil {
|
|
return nil, err
|
|
}
|
|
if d.imageMounter, err = d.lockdown.ImageMounterService(); err != nil {
|
|
return nil, err
|
|
}
|
|
imageMounter = d.imageMounter
|
|
return
|
|
}
|
|
|
|
func (d *device) Images(imgType ...string) (imageSignatures [][]byte, err error) {
|
|
if _, err = d.imageMounterService(); err != nil {
|
|
return nil, err
|
|
}
|
|
if len(imgType) == 0 {
|
|
imgType = []string{"Developer"}
|
|
}
|
|
return d.imageMounter.Images(imgType[0])
|
|
}
|
|
|
|
func (d *device) MountDeveloperDiskImage(dmgPath string, signaturePath string) (err error) {
|
|
if _, err = d.imageMounterService(); err != nil {
|
|
return err
|
|
}
|
|
devImgPath := "/private/var/mobile/Media/PublicStaging/staging.dimage"
|
|
return d.imageMounter.UploadImageAndMount("Developer", devImgPath, dmgPath, signaturePath)
|
|
}
|
|
|
|
func (d *device) screenshotService() (screenshot Screenshot, err error) {
|
|
if d.screenshot != nil {
|
|
return d.screenshot, nil
|
|
}
|
|
|
|
if _, err = d.lockdownService(); err != nil {
|
|
return nil, err
|
|
}
|
|
if d.screenshot, err = d.lockdown.ScreenshotService(); err != nil {
|
|
return nil, err
|
|
}
|
|
screenshot = d.screenshot
|
|
return
|
|
}
|
|
|
|
func (d *device) Screenshot() (raw *bytes.Buffer, err error) {
|
|
if _, err = d.screenshotService(); err != nil {
|
|
return nil, err
|
|
}
|
|
return d.screenshot.Take()
|
|
}
|
|
|
|
func (d *device) simulateLocationService() (simulateLocation SimulateLocation, err error) {
|
|
if d.simulateLocation != nil {
|
|
return d.simulateLocation, nil
|
|
}
|
|
if _, err = d.lockdownService(); err != nil {
|
|
return nil, err
|
|
}
|
|
if d.simulateLocation, err = d.lockdown.SimulateLocationService(); err != nil {
|
|
return nil, err
|
|
}
|
|
simulateLocation = d.simulateLocation
|
|
return
|
|
}
|
|
|
|
func (d *device) SimulateLocationUpdate(longitude float64, latitude float64, coordinateSystem ...CoordinateSystem) (err error) {
|
|
if _, err = d.simulateLocationService(); err != nil {
|
|
return err
|
|
}
|
|
return d.simulateLocation.Update(longitude, latitude, coordinateSystem...)
|
|
}
|
|
|
|
func (d *device) SimulateLocationRecover() (err error) {
|
|
if _, err = d.simulateLocationService(); err != nil {
|
|
return err
|
|
}
|
|
return d.simulateLocation.Recover()
|
|
}
|
|
|
|
func (d *device) installationProxyService() (installationProxy InstallationProxy, err error) {
|
|
if d.installationProxy != nil {
|
|
return d.installationProxy, nil
|
|
}
|
|
if _, err = d.lockdownService(); err != nil {
|
|
return nil, err
|
|
}
|
|
if d.installationProxy, err = d.lockdown.InstallationProxyService(); err != nil {
|
|
return nil, err
|
|
}
|
|
installationProxy = d.installationProxy
|
|
return
|
|
}
|
|
|
|
func (d *device) InstallationProxyBrowse(opts ...InstallationProxyOption) (currentList []interface{}, err error) {
|
|
if _, err = d.installationProxyService(); err != nil {
|
|
return nil, err
|
|
}
|
|
return d.installationProxy.Browse(opts...)
|
|
}
|
|
|
|
func (d *device) InstallationProxyLookup(opts ...InstallationProxyOption) (lookupResult interface{}, err error) {
|
|
if _, err = d.installationProxyService(); err != nil {
|
|
return nil, err
|
|
}
|
|
return d.installationProxy.Lookup(opts...)
|
|
}
|
|
|
|
func (d *device) newInstrumentsService() (instruments Instruments, err error) {
|
|
// NOTICE: each instruments service should have individual connection, otherwise it will be blocked
|
|
if _, err = d.lockdownService(); err != nil {
|
|
return
|
|
}
|
|
return d.lockdown.InstrumentsService()
|
|
}
|
|
|
|
func (d *device) instrumentsService() (instruments Instruments, err error) {
|
|
if d.instruments != nil {
|
|
return d.instruments, nil
|
|
}
|
|
if d.instruments, err = d.newInstrumentsService(); err != nil {
|
|
return nil, err
|
|
}
|
|
instruments = d.instruments
|
|
return
|
|
}
|
|
|
|
func (d *device) AppLaunch(bundleID string, opts ...AppLaunchOption) (pid int, err error) {
|
|
if _, err = d.instrumentsService(); err != nil {
|
|
return 0, err
|
|
}
|
|
return d.instruments.AppLaunch(bundleID, opts...)
|
|
}
|
|
|
|
func (d *device) AppKill(pid int) (err error) {
|
|
if _, err = d.instrumentsService(); err != nil {
|
|
return err
|
|
}
|
|
return d.instruments.AppKill(pid)
|
|
}
|
|
|
|
func (d *device) AppRunningProcesses() (processes []Process, err error) {
|
|
if _, err = d.instrumentsService(); err != nil {
|
|
return nil, err
|
|
}
|
|
return d.instruments.AppRunningProcesses()
|
|
}
|
|
|
|
func (d *device) AppList(opts ...AppListOption) (apps []Application, err error) {
|
|
if _, err = d.instrumentsService(); err != nil {
|
|
return nil, err
|
|
}
|
|
return d.instruments.AppList(opts...)
|
|
}
|
|
|
|
func (d *device) DeviceInfo() (devInfo *DeviceInfo, err error) {
|
|
if _, err = d.instrumentsService(); err != nil {
|
|
return nil, err
|
|
}
|
|
return d.instruments.DeviceInfo()
|
|
}
|
|
|
|
func (d *device) testmanagerdService() (testmanagerd Testmanagerd, err error) {
|
|
if _, err = d.lockdownService(); err != nil {
|
|
return nil, err
|
|
}
|
|
if testmanagerd, err = d.lockdown.TestmanagerdService(); err != nil {
|
|
return nil, err
|
|
}
|
|
return
|
|
}
|
|
|
|
func (d *device) AfcService() (afc Afc, err error) {
|
|
if d.afc != nil {
|
|
return d.afc, nil
|
|
}
|
|
if _, err = d.lockdownService(); err != nil {
|
|
return nil, err
|
|
}
|
|
if d.afc, err = d.lockdown.AfcService(); err != nil {
|
|
return nil, err
|
|
}
|
|
afc = d.afc
|
|
return
|
|
}
|
|
|
|
func (d *device) AppInstall(ipaPath string) (err error) {
|
|
if _, err = d.AfcService(); err != nil {
|
|
return err
|
|
}
|
|
|
|
stagingPath := "PublicStaging"
|
|
if _, err = d.afc.Stat(stagingPath); err != nil {
|
|
if err != ErrAfcStatNotExist {
|
|
return err
|
|
}
|
|
if err = d.afc.Mkdir(stagingPath); err != nil {
|
|
return fmt.Errorf("app install: %w", err)
|
|
}
|
|
}
|
|
|
|
var info map[string]interface{}
|
|
if info, err = ipa.Info(ipaPath); err != nil {
|
|
return err
|
|
}
|
|
bundleID, ok := info["CFBundleIdentifier"]
|
|
if !ok {
|
|
return errors.New("can't find 'CFBundleIdentifier'")
|
|
}
|
|
|
|
installationPath := path.Join(stagingPath, fmt.Sprintf("%s.ipa", bundleID))
|
|
|
|
var data []byte
|
|
if data, err = os.ReadFile(ipaPath); err != nil {
|
|
return err
|
|
}
|
|
if err = d.afc.WriteFile(installationPath, data, AfcFileModeWr); err != nil {
|
|
return err
|
|
}
|
|
|
|
if _, err = d.installationProxyService(); err != nil {
|
|
return err
|
|
}
|
|
|
|
return d.installationProxy.Install(fmt.Sprintf("%s", bundleID), installationPath)
|
|
}
|
|
|
|
func (d *device) AppUninstall(bundleID string) (err error) {
|
|
if _, err = d.installationProxyService(); err != nil {
|
|
return err
|
|
}
|
|
|
|
return d.installationProxy.Uninstall(bundleID)
|
|
}
|
|
|
|
func (d *device) HouseArrestService() (houseArrest HouseArrest, err error) {
|
|
if d.houseArrest != nil {
|
|
return d.houseArrest, nil
|
|
}
|
|
if _, err = d.lockdownService(); err != nil {
|
|
return nil, err
|
|
}
|
|
if d.houseArrest, err = d.lockdown.HouseArrestService(); err != nil {
|
|
return nil, err
|
|
}
|
|
houseArrest = d.houseArrest
|
|
return
|
|
}
|
|
|
|
func (d *device) syslogRelayService() (syslogRelay SyslogRelay, err error) {
|
|
if d.syslogRelay != nil {
|
|
return d.syslogRelay, nil
|
|
}
|
|
if _, err = d.lockdownService(); err != nil {
|
|
return nil, err
|
|
}
|
|
if d.syslogRelay, err = d.lockdown.SyslogRelayService(); err != nil {
|
|
return nil, err
|
|
}
|
|
syslogRelay = d.syslogRelay
|
|
return
|
|
}
|
|
|
|
func (d *device) Syslog() (lines <-chan string, err error) {
|
|
if _, err = d.syslogRelayService(); err != nil {
|
|
return nil, err
|
|
}
|
|
return d.syslogRelay.Lines(), nil
|
|
}
|
|
|
|
func (d *device) SyslogStop() {
|
|
if d.syslogRelay == nil {
|
|
return
|
|
}
|
|
d.syslogRelay.Stop()
|
|
}
|
|
|
|
func (d *device) Reboot() (err error) {
|
|
if _, err = d.lockdownService(); err != nil {
|
|
return
|
|
}
|
|
if d.diagnosticsRelay, err = d.lockdown.DiagnosticsRelayService(); err != nil {
|
|
return
|
|
}
|
|
if err = d.diagnosticsRelay.Reboot(); err != nil {
|
|
return
|
|
}
|
|
return
|
|
}
|
|
|
|
func (d *device) Shutdown() (err error) {
|
|
if _, err = d.lockdownService(); err != nil {
|
|
return
|
|
}
|
|
if d.diagnosticsRelay, err = d.lockdown.DiagnosticsRelayService(); err != nil {
|
|
return
|
|
}
|
|
if err = d.diagnosticsRelay.Shutdown(); err != nil {
|
|
return
|
|
}
|
|
return
|
|
}
|
|
|
|
func (d *device) springBoardService() (springBoard SpringBoard, err error) {
|
|
if d.springBoard != nil {
|
|
return d.springBoard, nil
|
|
}
|
|
if _, err = d.lockdownService(); err != nil {
|
|
return nil, err
|
|
}
|
|
if d.springBoard, err = d.lockdown.SpringBoardService(); err != nil {
|
|
return nil, err
|
|
}
|
|
springBoard = d.springBoard
|
|
return
|
|
}
|
|
|
|
func (d *device) GetIconPNGData(bundleId string) (raw *bytes.Buffer, err error) {
|
|
if _, err = d.lockdownService(); err != nil {
|
|
return
|
|
}
|
|
if d.springBoard, err = d.lockdown.SpringBoardService(); err != nil {
|
|
return
|
|
}
|
|
if raw, err = d.springBoard.GetIconPNGData(bundleId); err != nil {
|
|
return
|
|
}
|
|
return
|
|
}
|
|
|
|
func (d *device) GetInterfaceOrientation() (orientation libimobiledevice.OrientationState, err error) {
|
|
if _, err = d.springBoardService(); err != nil {
|
|
return
|
|
}
|
|
if orientation, err = d.springBoard.GetInterfaceOrientation(); err != nil {
|
|
return
|
|
}
|
|
return
|
|
}
|
|
|
|
func (d *device) PcapStart(opts ...PcapOption) (lines <-chan []byte, err error) {
|
|
pcapOptions := &PcapOptions{}
|
|
for _, fn := range opts {
|
|
fn(pcapOptions)
|
|
}
|
|
log.Info().Interface("options", pcapOptions).Msg("pcap start")
|
|
|
|
// wait until get pid for bundle id
|
|
if pcapOptions.BundleID != "" {
|
|
instruments, err := d.newInstrumentsService()
|
|
if err != nil {
|
|
fmt.Printf("get pid by bundle id failed: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
for {
|
|
pid, err := instruments.getPidByBundleID(pcapOptions.BundleID)
|
|
if err != nil {
|
|
time.Sleep(1 * time.Second)
|
|
continue
|
|
}
|
|
pcapOptions.Pid = pid
|
|
break
|
|
}
|
|
}
|
|
|
|
if d.pcapd == nil {
|
|
if _, err = d.lockdownService(); err != nil {
|
|
return nil, err
|
|
}
|
|
if d.pcapd, err = d.lockdown.PcapdService(
|
|
pcapOptions.Pid, pcapOptions.ProcName); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return d.pcapd.Packet(), nil
|
|
}
|
|
|
|
func (d *device) PcapStop() {
|
|
if d.pcapd == nil {
|
|
return
|
|
}
|
|
d.pcapd.Stop()
|
|
}
|
|
|
|
func (d *device) PerfStart(opts ...PerfOption) (data <-chan []byte, err error) {
|
|
perfOptions := defaulPerfOption()
|
|
for _, fn := range opts {
|
|
fn(perfOptions)
|
|
}
|
|
|
|
// wait until get pid for bundle id
|
|
if perfOptions.BundleID != "" {
|
|
instruments, err := d.newInstrumentsService()
|
|
if err != nil {
|
|
fmt.Printf("get pid by bundle id failed: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
for {
|
|
pid, err := instruments.getPidByBundleID(perfOptions.BundleID)
|
|
if err != nil {
|
|
time.Sleep(1 * time.Second)
|
|
continue
|
|
}
|
|
perfOptions.Pid = pid
|
|
break
|
|
}
|
|
}
|
|
|
|
// processAttributes must contain pid, or it can't get process info, reason unknown
|
|
if !containString(perfOptions.ProcessAttributes, "pid") {
|
|
perfOptions.ProcessAttributes = append(perfOptions.ProcessAttributes, "pid")
|
|
}
|
|
|
|
outCh := make(chan []byte, 100)
|
|
|
|
if perfOptions.SysCPU || perfOptions.SysMem || perfOptions.SysDisk ||
|
|
perfOptions.SysNetwork {
|
|
perfd, err := d.newPerfdSysmontap(perfOptions)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
data, err := perfd.Start()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
go func() {
|
|
for {
|
|
outCh <- (<-data)
|
|
}
|
|
}()
|
|
d.perfd = append(d.perfd, perfd)
|
|
}
|
|
|
|
if perfOptions.Network {
|
|
perfd, err := d.newPerfdNetworking(perfOptions)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
data, err := perfd.Start()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
go func() {
|
|
for {
|
|
outCh <- (<-data)
|
|
}
|
|
}()
|
|
d.perfd = append(d.perfd, perfd)
|
|
}
|
|
|
|
if perfOptions.FPS || perfOptions.gpu {
|
|
perfd, err := d.newPerfdGraphicsOpengl(perfOptions)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
data, err := perfd.Start()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
go func() {
|
|
for {
|
|
outCh <- (<-data)
|
|
}
|
|
}()
|
|
d.perfd = append(d.perfd, perfd)
|
|
}
|
|
|
|
return outCh, nil
|
|
}
|
|
|
|
func (d *device) PerfStop() {
|
|
if d.perfd == nil {
|
|
return
|
|
}
|
|
for _, p := range d.perfd {
|
|
p.Stop()
|
|
}
|
|
}
|
|
|
|
func (d *device) crashReportMoverService() (crashReportMover CrashReportMover, err error) {
|
|
if d.crashReportMover != nil {
|
|
return d.crashReportMover, nil
|
|
}
|
|
if _, err = d.lockdownService(); err != nil {
|
|
return nil, err
|
|
}
|
|
if d.crashReportMover, err = d.lockdown.CrashReportMoverService(); err != nil {
|
|
return nil, err
|
|
}
|
|
crashReportMover = d.crashReportMover
|
|
return
|
|
}
|
|
|
|
func (d *device) MoveCrashReport(hostDir string, opts ...CrashReportMoverOption) (err error) {
|
|
if _, err = d.crashReportMoverService(); err != nil {
|
|
return err
|
|
}
|
|
return d.crashReportMover.Move(hostDir, opts...)
|
|
}
|
|
|
|
func (d *device) XCTest(bundleID string, opts ...XCTestOption) (out <-chan string, cancel context.CancelFunc, err error) {
|
|
xcTestOpt := defaultXCTestOption()
|
|
for _, fn := range opts {
|
|
fn(xcTestOpt)
|
|
}
|
|
|
|
ctx, cancelFunc := context.WithCancel(context.TODO())
|
|
_out := make(chan string)
|
|
|
|
xcodeVersion := uint64(30)
|
|
|
|
var tmSrv1 Testmanagerd
|
|
if tmSrv1, err = d.testmanagerdService(); err != nil {
|
|
return _out, cancelFunc, err
|
|
}
|
|
|
|
var xcTestManager1 XCTestManagerDaemon
|
|
if xcTestManager1, err = tmSrv1.newXCTestManagerDaemon(); err != nil {
|
|
return _out, cancelFunc, err
|
|
}
|
|
|
|
var version []int
|
|
if version, err = d.lockdown._getProductVersion(); err != nil {
|
|
return _out, cancelFunc, err
|
|
}
|
|
|
|
if DeviceVersion(version...) >= DeviceVersion(11, 0, 0) {
|
|
if err = xcTestManager1.initiateControlSession(xcodeVersion); err != nil {
|
|
return _out, cancelFunc, err
|
|
}
|
|
}
|
|
|
|
var tmSrv2 Testmanagerd
|
|
if tmSrv2, err = d.testmanagerdService(); err != nil {
|
|
return _out, cancelFunc, err
|
|
}
|
|
|
|
var xcTestManager2 XCTestManagerDaemon
|
|
if xcTestManager2, err = tmSrv2.newXCTestManagerDaemon(); err != nil {
|
|
return _out, cancelFunc, err
|
|
}
|
|
|
|
xcTestManager2.registerCallback("_XCT_logDebugMessage:", func(m libimobiledevice.DTXMessageResult) {
|
|
// more information ( each operation )
|
|
// fmt.Println("###### xcTestManager2 ### -->", m)
|
|
if strings.Contains(fmt.Sprintf("%s", m), "Received test runner ready reply with error: (null)") {
|
|
// fmt.Println("###### xcTestManager2 ### -->", fmt.Sprintf("%v", m.Aux[0]))
|
|
time.Sleep(time.Second)
|
|
if err = xcTestManager2.startExecutingTestPlan(xcodeVersion); err != nil {
|
|
debugLog(fmt.Sprintf("startExecutingTestPlan %d: %s", xcodeVersion, err))
|
|
return
|
|
}
|
|
}
|
|
})
|
|
xcTestManager2.registerCallback("_Golang-iDevice_Unregistered", func(m libimobiledevice.DTXMessageResult) {
|
|
// more information
|
|
// _XCT_testRunnerReadyWithCapabilities:
|
|
// _XCT_didBeginExecutingTestPlan
|
|
// _XCT_didBeginInitializingForUITesting
|
|
// _XCT_testSuite:didStartAt:
|
|
// _XCT_testCase:method:willStartActivity:
|
|
// _XCT_testCase:method:didFinishActivity:
|
|
// _XCT_testCaseDidStartForTestClass:method:
|
|
// fmt.Println("###### xcTestManager2 ### _Unregistered -->", m)
|
|
})
|
|
|
|
sessionId := uuid.NewV4()
|
|
if err = xcTestManager2.initiateSession(xcodeVersion, nskeyedarchiver.NewNSUUID(sessionId.Bytes())); err != nil {
|
|
return _out, cancelFunc, err
|
|
}
|
|
|
|
if _, err = d.installationProxyService(); err != nil {
|
|
return _out, cancelFunc, err
|
|
}
|
|
|
|
var vResult interface{}
|
|
if vResult, err = d.installationProxy.Lookup(WithBundleIDs(bundleID)); err != nil {
|
|
return _out, cancelFunc, err
|
|
}
|
|
|
|
lookupResult := vResult.(map[string]interface{})
|
|
lookupResult = lookupResult[bundleID].(map[string]interface{})
|
|
appContainer := lookupResult["Container"].(string)
|
|
appPath := lookupResult["Path"].(string)
|
|
|
|
var pathXCTestCfg string
|
|
if pathXCTestCfg, err = d._uploadXCTestConfiguration(bundleID, sessionId, lookupResult); err != nil {
|
|
return _out, cancelFunc, err
|
|
}
|
|
|
|
if _, err = d.instrumentsService(); err != nil {
|
|
return _out, cancelFunc, err
|
|
}
|
|
|
|
if err = d.instruments.appProcess(bundleID); err != nil {
|
|
return _out, cancelFunc, err
|
|
}
|
|
|
|
pathXCTestConfiguration := appContainer + pathXCTestCfg
|
|
|
|
appEnv := map[string]interface{}{
|
|
"CA_ASSERT_MAIN_THREAD_TRANSACTIONS": "0",
|
|
"CA_DEBUG_TRANSACTIONS": "0",
|
|
"DYLD_FRAMEWORK_PATH": appPath + "/Frameworks:",
|
|
"DYLD_LIBRARY_PATH": appPath + "/Frameworks",
|
|
"NSUnbufferedIO": "YES",
|
|
"SQLITE_ENABLE_THREAD_ASSERTIONS": "1",
|
|
"WDA_PRODUCT_BUNDLE_IDENTIFIER": "",
|
|
"XCTestConfigurationFilePath": pathXCTestConfiguration, // Running tests with active test configuration:
|
|
// "XCTestBundlePath": fmt.Sprintf("%s/PlugIns/%s.xctest", appPath, name), // !!! ERROR
|
|
// "XCTestSessionIdentifier": sessionId.String(), // !!! ERROR
|
|
// "XCTestSessionIdentifier": "",
|
|
"XCODE_DBG_XPC_EXCLUSIONS": "com.apple.dt.xctestSymbolicator",
|
|
"MJPEG_SERVER_PORT": "",
|
|
"USE_PORT": "",
|
|
"LLVM_PROFILE_FILE": appContainer + "/tmp/%p.profraw",
|
|
}
|
|
if DeviceVersion(version...) >= DeviceVersion(11, 0, 0) {
|
|
appEnv["DYLD_INSERT_LIBRARIES"] = "/Developer/usr/lib/libMainThreadChecker.dylib"
|
|
appEnv["OS_ACTIVITY_DT_MODE"] = "YES"
|
|
}
|
|
appArgs := []interface{}{
|
|
"-NSTreatUnknownArgumentsAsOpen", "NO",
|
|
"-ApplePersistenceIgnoreState", "YES",
|
|
}
|
|
appOpt := map[string]interface{}{
|
|
"StartSuspendedKey": uint64(0),
|
|
}
|
|
if DeviceVersion(version...) >= DeviceVersion(12, 0, 0) {
|
|
appOpt["ActivateSuspended"] = uint64(1)
|
|
}
|
|
|
|
if len(xcTestOpt.appEnv) != 0 {
|
|
for k, v := range xcTestOpt.appEnv {
|
|
appEnv[k] = v
|
|
}
|
|
}
|
|
|
|
if len(xcTestOpt.appOpt) != 0 {
|
|
for k, v := range xcTestOpt.appEnv {
|
|
appOpt[k] = v
|
|
}
|
|
}
|
|
|
|
d.instruments.registerCallback("outputReceived:fromProcess:atTime:", func(m libimobiledevice.DTXMessageResult) {
|
|
// fmt.Println("###### instruments ### -->", m.Aux[0])
|
|
_out <- fmt.Sprintf("%s", m.Aux[0])
|
|
})
|
|
|
|
var pid int
|
|
if pid, err = d.instruments.AppLaunch(bundleID,
|
|
WithAppPath(appPath),
|
|
WithEnvironment(appEnv),
|
|
WithArguments(appArgs),
|
|
WithOptions(appOpt),
|
|
WithKillExisting(true),
|
|
); err != nil {
|
|
return _out, cancelFunc, err
|
|
}
|
|
|
|
// see https://github.com/httprunner/httprunner/v4/hrp/pkg/gidevice/issues/31
|
|
// if err = d.instruments.startObserving(pid); err != nil {
|
|
// return _out, cancelFunc, err
|
|
// }
|
|
|
|
if DeviceVersion(version...) >= DeviceVersion(12, 0, 0) {
|
|
err = xcTestManager1.authorizeTestSession(pid)
|
|
} else if DeviceVersion(version...) <= DeviceVersion(9, 0, 0) {
|
|
err = xcTestManager1.initiateControlSessionForTestProcessID(pid)
|
|
} else {
|
|
err = xcTestManager1.initiateControlSessionForTestProcessIDProtocolVersion(pid, xcodeVersion)
|
|
}
|
|
if err != nil {
|
|
return _out, cancelFunc, err
|
|
}
|
|
|
|
go func() {
|
|
d.instruments.registerCallback("_Golang-iDevice_Over", func(_ libimobiledevice.DTXMessageResult) {
|
|
cancelFunc()
|
|
})
|
|
|
|
<-ctx.Done()
|
|
tmSrv1.close()
|
|
tmSrv2.close()
|
|
xcTestManager1.close()
|
|
xcTestManager2.close()
|
|
if _err := d.AppKill(pid); _err != nil {
|
|
debugLog(fmt.Sprintf("xctest kill: %d", pid))
|
|
}
|
|
// time.Sleep(time.Second)
|
|
close(_out)
|
|
}()
|
|
|
|
return _out, cancelFunc, err
|
|
}
|
|
|
|
func (d *device) _uploadXCTestConfiguration(bundleID string, sessionId uuid.UUID, lookupResult map[string]interface{}) (pathXCTestCfg string, err error) {
|
|
if _, err = d.HouseArrestService(); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
var appAfc Afc
|
|
if appAfc, err = d.houseArrest.Container(bundleID); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
appTmpFilenames, err := appAfc.ReadDir("/tmp")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
for _, tName := range appTmpFilenames {
|
|
if strings.HasSuffix(tName, ".xctestconfiguration") {
|
|
if _err := appAfc.Remove(fmt.Sprintf("/tmp/%s", tName)); _err != nil {
|
|
debugLog(fmt.Sprintf("remove /tmp/%s: %s", tName, err))
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
|
|
nameExec := lookupResult["CFBundleExecutable"].(string)
|
|
name := nameExec[:len(nameExec)-len("-Runner")]
|
|
appPath := lookupResult["Path"].(string)
|
|
|
|
pathXCTestCfg = fmt.Sprintf("/tmp/%s-%s.xctestconfiguration", name, strings.ToUpper(sessionId.String()))
|
|
|
|
var content []byte
|
|
if content, err = nskeyedarchiver.Marshal(
|
|
nskeyedarchiver.NewXCTestConfiguration(
|
|
nskeyedarchiver.NewNSUUID(sessionId.Bytes()),
|
|
nskeyedarchiver.NewNSURL(fmt.Sprintf("%s/PlugIns/%s.xctest", appPath, name)),
|
|
bundleID,
|
|
appPath,
|
|
),
|
|
); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if err = appAfc.WriteFile(pathXCTestCfg, content, AfcFileModeWr); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return
|
|
}
|