UNPKG

kuben-appium-xcuitest-driver

Version:

Appium driver for iOS using XCUITest for backend

223 lines (201 loc) 8.84 kB
import _ from 'lodash'; import path from 'path'; import { fs, tempDir } from 'appium-support'; import { SubProcess, exec } from 'teen_process'; import log from '../logger'; import { encodeBase64OrUpload } from '../utils'; import { waitForCondition } from 'asyncbox'; let commands = {}; const RECORDERS_CACHE = {}; const DEFAULT_TIMEOUT_MS = 5 * 60 * 1000; const STOP_TIMEOUT_MS = 3 * 60 * 1000; const START_TIMEOUT_MS = 15 * 1000; const DEFAULT_PROFILE_NAME = 'Activity Monitor'; const DEFAULT_EXT = '.trace'; async function finishPerfRecord (proc, stopGracefully = true) { if (!proc.isRunning) { return; } if (stopGracefully) { log.debug(`Sending SIGINT to the running instruments process`); return await proc.stop('SIGINT', STOP_TIMEOUT_MS); } log.debug(`Sending SIGTERM to the running instruments process`); await proc.stop(); } async function uploadTrace (localFile, remotePath = null, uploadOptions = {}) { try { return await encodeBase64OrUpload(localFile, remotePath, uploadOptions); } finally { await fs.rimraf(localFile); } } /** * @typedef {Object} StartPerfRecordOptions * * @property {?number|string} timeout [300000] - The maximum count of milliseconds to record the profiling information. * @property {?string} profileName [Activity Monitor] - The name of existing performance profile to apply. * Execute `instruments -s` to show the list of available profiles. * Note, that not all profiles are supported on mobile devices. * @property {?string|number} pid - The ID of the process to meassure the performance for. * Set it to `current` in order to meassure the performance of * the process, which belongs to the currently active application. * All processes running on the device are meassured if * pid is unset (the default setting). */ /** * Starts performance profiling for the device under test. * The `instruments` developer utility is used for this purpose under the hood. * It is possible to record multiple profiles at the same time. * Read https://developer.apple.com/library/content/documentation/DeveloperTools/Conceptual/InstrumentsUserGuide/Recording,Pausing,andStoppingTraces.html * for more details. * * @param {?StartPerfRecordOptions} opts - The set of possible start record options */ commands.mobileStartPerfRecord = async function (opts = {}) { if (!this.relaxedSecurityEnabled && !this.isRealDevice()) { log.errorAndThrow(`Appium server must have relaxed security flag set in order ` + `for Simulator performance measurement to work`); } const { timeout = DEFAULT_TIMEOUT_MS, profileName = DEFAULT_PROFILE_NAME, pid, } = opts; // Cleanup the process if it is already running const runningRecorders = RECORDERS_CACHE[profileName]; if (_.isPlainObject(runningRecorders) && runningRecorders[this.opts.device.udid]) { const {proc, localPath} = runningRecorders[this.opts.device.udid]; await finishPerfRecord(proc, false); if (await fs.exists(localPath)) { await fs.rimraf(localPath); } delete runningRecorders[this.opts.device.udid]; } if (!await fs.which('instruments')) { log.errorAndThrow(`Cannot start performance recording, because 'instruments' ` + `tool cannot be found in PATH. Are Xcode development tools installed?`); } const localPath = await tempDir.path({ prefix: `appium_perf_${profileName}_${Date.now()}`.replace(/\W/g, '_'), suffix: DEFAULT_EXT, }); const args = [ '-w', this.opts.device.udid, '-t', profileName, '-D', localPath, '-l', timeout, ]; if (pid) { if (`${pid}`.toLowerCase() === 'current') { const appInfo = await this.proxyCommand('/wda/activeAppInfo', 'GET'); args.push('-p', appInfo.pid); } else { args.push('-p', pid); } } const proc = new SubProcess('instruments', args); log.info(`Starting 'instruments' with arguments: ${args.join(' ')}`); proc.on('exit', (code) => { const msg = `instruments exited with code '${code}'`; if (code) { log.warn(msg); } else { log.debug(msg); } }); proc.on('output', (stdout, stderr) => { (stdout || stderr).split('\n') .filter(x => x.length) .map(x => log.debug(`[instruments] ${x}`)); }); await proc.start(0); try { await waitForCondition(async () => await fs.exists(localPath), { waitMs: START_TIMEOUT_MS, intervalMs: 500, }); } catch (err) { try { await proc.stop('SIGKILL'); } catch (ign) {} log.errorAndThrow(`Cannot start performance monitoring for '${profileName}' profile in ${START_TIMEOUT_MS}ms. ` + `Make sure you can execute it manually.`); } RECORDERS_CACHE[profileName] = Object.assign({}, (RECORDERS_CACHE[profileName] || {}), { [this.opts.device.udid]: {proc, localPath}, }); }; /** * @typedef {Object} StopRecordingOptions * * @property {?string} remotePath - The path to the remote location, where the resulting zipped .trace file should be uploaded. * The following protocols are supported: http/https, ftp. * Null or empty string value (the default setting) means the content of resulting * file should be zipped, encoded as Base64 and passed as the endpount response value. * An exception will be thrown if the generated file is too big to * fit into the available process memory. * @property {?string} user - The name of the user for the remote authentication. Only works if `remotePath` is provided. * @property {?string} pass - The password for the remote authentication. Only works if `remotePath` is provided. * @property {?string} method [PUT] - The http multipart upload method name. Only works if `remotePath` is provided. * @property {?string} profileName [Activity Monitor] - The name of an existing performance profile for which the recording has been made. */ /** * Stops performance profiling for the device under test. * The resulting file in .trace format can be either returned * directly as base64-encoded zip archive or uploaded to a remote location * (such files can be pretty large). Afterwards it is possible to unarchive and * open such file with Xcode Dev Tools. * * @param {?StopRecordingOptions} opts - The set of possible stop record options * @return {string} Either an empty string if the upload wqaas successful or base-64 encoded * content of zipped .trace file. * @throws {Error} If no performance recording with given profile name/device udid combination * has been started before or the resulting .trace file has not been generated properly. */ commands.mobileStopPerfRecord = async function (opts = {}) { if (!this.relaxedSecurityEnabled && !this.isRealDevice()) { log.errorAndThrow(`Appium server must have relaxed security flag set in order ` + `for Simulator performance measurement to work`); } const { remotePath, user, pass, method, profileName = DEFAULT_PROFILE_NAME, } = opts; const runningRecorders = RECORDERS_CACHE[profileName]; if (!_.isPlainObject(runningRecorders) || !runningRecorders[this.opts.device.udid]) { log.errorAndThrow(`There are no records for performance profile '${profileName}' ` + `and device ${this.opts.device.udid}. ` + `Have you started the profiling before?`); } const {proc, localPath} = runningRecorders[this.opts.device.udid]; await finishPerfRecord(proc, true); if (!await fs.exists(localPath)) { log.errorAndThrow(`There is no .trace file found for performance profile '${profileName}' ` + `and device ${this.opts.device.udid}. ` + `Make sure the profile is supported on this device. ` + `You can use 'instruments -s' command to see the list of all available profiles.`); } const zipPath = `${localPath}.zip`; const zipArgs = [ '-9', '-r', zipPath, path.basename(localPath), ]; log.info(`Found perf trace record '${localPath}'. Compressing it with 'zip ${zipArgs.join(' ')}'`); try { await exec('zip', zipArgs, { cwd: path.dirname(localPath), }); return await uploadTrace(zipPath, remotePath, {user, pass, method}); } finally { delete runningRecorders[this.opts.device.udid]; if (await fs.exists(localPath)) { await fs.rimraf(localPath); } } }; export { commands }; export default commands;