UNPKG

appium-xcuitest-driver

Version:

Appium driver for iOS using XCUITest for backend

167 lines (146 loc) 5.32 kB
import {fs, tempDir, util} from 'appium/support'; import B from 'bluebird'; import path from 'path'; import _ from 'lodash'; import {Pyidevice} from '../real-device-clients/py-ios-device-client'; import {IOSLog} from './ios-log'; import { toLogEntry, grepFile } from './helpers'; import type { AppiumLogger } from '@appium/types'; import type { BaseDeviceClient } from '../real-device-clients/base-device-client'; import type { Simulator } from 'appium-ios-simulator'; import type { LogEntry } from '../commands/types'; // The file format has been changed from '.crash' to '.ips' since Monterey. const CRASH_REPORTS_GLOB_PATTERN = '**/*.@(crash|ips)'; // The size of a single diagnostic report might be hundreds of kilobytes. // Thus we do not want to store too many items in the memory at once. const MAX_RECENT_ITEMS = 20; type TSerializedEntry = [string, number]; export interface IOSCrashLogOptions { /** UDID of a real device */ udid?: string; /** Simulator instance */ sim?: Simulator; log: AppiumLogger; } export class IOSCrashLog extends IOSLog<TSerializedEntry, TSerializedEntry> { private readonly _udid: string | undefined; private readonly _realDeviceClient: BaseDeviceClient | null; private readonly _logDir: string | null; private readonly _sim: Simulator | undefined; private _recentCrashFiles: string[]; private _started: boolean; constructor(opts: IOSCrashLogOptions) { super({ log: opts.log, maxBufferSize: MAX_RECENT_ITEMS, }); this._udid = opts.udid; this._sim = opts.sim; this._realDeviceClient = this._isRealDevice() ? new Pyidevice({ udid: this._udid as string, log: opts.log, }) : null; this._logDir = this._isRealDevice() ? null : path.resolve(process.env.HOME || '/', 'Library', 'Logs', 'DiagnosticReports'); this._recentCrashFiles = []; this._started = false; } override async startCapture(): Promise<void> { this._recentCrashFiles = await this._listCrashFiles(false); this._started = true; } override async stopCapture(): Promise<void> { this._started = false; } override get isCapturing(): boolean { return this._started; } override async getLogs(): Promise<LogEntry[]> { const crashFiles = (await this._listCrashFiles(true)).slice(-MAX_RECENT_ITEMS); const diffFiles = _.difference(crashFiles, this._recentCrashFiles); if (_.isEmpty(diffFiles)) { return []; } this.log.debug(`Found ${util.pluralize('fresh crash report', diffFiles.length, true)}`); await this._serializeCrashes(diffFiles); this._recentCrashFiles = crashFiles; return super.getLogs(); } protected override _serializeEntry(value: TSerializedEntry): TSerializedEntry { return value; } protected override _deserializeEntry(value: TSerializedEntry): LogEntry { const [message, timestamp] = value; return toLogEntry(message, timestamp); } private async _serializeCrashes(paths: string[]): Promise<void> { const tmpRoot = await tempDir.openDir(); try { for (const filePath of paths) { let fullPath = filePath; if (this._isRealDevice()) { const fileName = filePath; try { await (this._realDeviceClient as BaseDeviceClient).exportCrash(fileName, tmpRoot); } catch (e) { this.log.warn( `Cannot export the crash report '${fileName}'. Skipping it. ` + `Original error: ${e.message}`, ); return; } fullPath = path.join(tmpRoot, fileName); } const {ctime} = await fs.stat(fullPath); this.broadcast([await fs.readFile(fullPath, 'utf8'), ctime.getTime()]); } } finally { await fs.rimraf(tmpRoot); } } private async _gatherFromRealDevice(strict: boolean): Promise<string[]> { if (!this._realDeviceClient) { return []; } if (!await this._realDeviceClient.assertExists(strict)) { this.log.info( `The ${_.toLower(this._realDeviceClient.constructor.name)} tool is not present in PATH. ` + `Skipping crash logs collection for real devices.` ); return []; } return await this._realDeviceClient.listCrashes(); } private async _gatherFromSimulator(): Promise<string[]> { if (!this._logDir || !this._sim || !(await fs.exists(this._logDir))) { this.log.debug(`Crash reports root '${this._logDir}' does not exist. Got nothing to gather.`); return []; } const foundFiles = await fs.glob(CRASH_REPORTS_GLOB_PATTERN, { cwd: this._logDir, absolute: true, }); const simUdid = (this._sim as Simulator).udid; // For Simulator only include files, that contain current UDID return await B.filter(foundFiles, async (filePath) => { try { return await grepFile(filePath, simUdid, {caseInsensitive: true}); } catch (err) { this.log.warn(err); return false; } }); } private async _listCrashFiles(strict: boolean): Promise<string[]> { return this._isRealDevice() ? await this._gatherFromRealDevice(strict) : await this._gatherFromSimulator(); } private _isRealDevice(): boolean { return Boolean(this._udid); } } export default IOSCrashLog;