UNPKG

appium-xcuitest-driver

Version:

Appium driver for iOS using XCUITest for backend

360 lines 15.4 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.IO_TIMEOUT_MS = void 0; exports.pullFile = pullFile; exports.pullFolder = pullFolder; exports.pushFile = pushFile; exports.pushFolder = pushFolder; const lodash_1 = __importDefault(require("lodash")); const bluebird_1 = __importStar(require("bluebird")); const support_1 = require("appium/support"); const path_1 = __importDefault(require("path")); const logger_1 = __importDefault(require("./logger")); exports.IO_TIMEOUT_MS = 4 * 60 * 1000; // Mobile devices use NAND memory modules for the storage, // and the parallelism there is not as performant as on regular SSDs const MAX_IO_CHUNK_SIZE = 8; /** * Retrieve a file from a real device * * @param {any} afcService Apple File Client service instance from * 'appium-ios-device' module * @param {string} remotePath Relative path to the file on the device * @returns {Promise<Buffer>} The file content as a buffer */ async function pullFile(afcService, remotePath) { const stream = await afcService.createReadStream(remotePath, { autoDestroy: true }); const pullPromise = new bluebird_1.default((resolve, reject) => { stream.on('close', resolve); stream.on('error', reject); }).timeout(exports.IO_TIMEOUT_MS); const buffers = []; stream.on('data', (data) => buffers.push(data)); await pullPromise; return Buffer.concat(buffers); } /** * Checks a presence of a local folder. * * @param {string} folderPath Full path to the local folder * @returns {Promise<boolean>} True if the folder exists and is actually a folder */ async function folderExists(folderPath) { try { return (await support_1.fs.stat(folderPath)).isDirectory(); } catch { return false; } } /** * Retrieve a folder from a real device * * @param {any} afcService Apple File Client service instance from * 'appium-ios-device' module * @param {string} remoteRootPath Relative path to the folder on the device * @returns {Promise<Buffer>} The folder content as a zipped base64-encoded buffer */ async function pullFolder(afcService, remoteRootPath) { const tmpFolder = await support_1.tempDir.openDir(); try { let localTopItem = null; let countFilesSuccess = 0; let countFilesFail = 0; let countFolders = 0; const pullPromises = []; await afcService.walkDir(remoteRootPath, true, async (remotePath, isDir) => { const localPath = path_1.default.join(tmpFolder, remotePath); const dirname = isDir ? localPath : path_1.default.dirname(localPath); if (!(await folderExists(dirname))) { await (0, support_1.mkdirp)(dirname); } if (!localTopItem || localPath.split(path_1.default.sep).length < localTopItem.split(path_1.default.sep).length) { localTopItem = localPath; } if (isDir) { ++countFolders; return; } const readStream = await afcService.createReadStream(remotePath, { autoDestroy: true }); const writeStream = support_1.fs.createWriteStream(localPath, { autoClose: true }); pullPromises.push(new bluebird_1.default((resolve) => { writeStream.on('close', () => { ++countFilesSuccess; resolve(); }); const onStreamingError = (e) => { readStream.unpipe(writeStream); logger_1.default.warn(`Cannot pull '${remotePath}' to '${localPath}'. ` + `The file will be skipped. Original error: ${e.message}`); ++countFilesFail; resolve(); }; writeStream.on('error', onStreamingError); readStream.on('error', onStreamingError); }).timeout(exports.IO_TIMEOUT_MS)); readStream.pipe(writeStream); if (pullPromises.length >= MAX_IO_CHUNK_SIZE) { await bluebird_1.default.any(pullPromises); } lodash_1.default.remove(pullPromises, (p) => p.isFulfilled()); }); // Wait for the rest of files to be pulled if (!lodash_1.default.isEmpty(pullPromises)) { await bluebird_1.default.all(pullPromises); } logger_1.default.info(`Pulled ${support_1.util.pluralize('file', countFilesSuccess, true)} out of ` + `${countFilesSuccess + countFilesFail} and ${support_1.util.pluralize('folder', countFolders, true)} ` + `from '${remoteRootPath}'`); return await support_1.zip.toInMemoryZip(localTopItem ? path_1.default.dirname(localTopItem) : tmpFolder, { encodeToBase64: true, }); } finally { await support_1.fs.rimraf(tmpFolder); } } /** * Creates remote folder path recursively. Noop if the given path * already exists * * @param {any} afcService Apple File Client service instance from * 'appium-ios-device' module * @param {string} remoteRoot The relative path to the remote folder structure * to be created */ async function remoteMkdirp(afcService, remoteRoot) { if (remoteRoot === '.' || remoteRoot === '/') { return; } try { await afcService.listDirectory(remoteRoot); return; } catch { // This means that the directory is missing and we got an object not found error. // Therefore, we are going to the parent await remoteMkdirp(afcService, path_1.default.dirname(remoteRoot)); } await afcService.createDirectory(remoteRoot); } /** * @typedef {Object} PushFileOptions * @property {number} [timeoutMs=240000] The maximum count of milliceconds to wait until * file push is completed. Cannot be lower than 60000ms */ /** * Pushes a file to a real device * * @param {any} afcService afcService Apple File Client service instance from * 'appium-ios-device' module * @param {string|Buffer} localPathOrPayload Either full path to the source file * or a buffer payload to be written into the remote destination * @param {string} remotePath Relative path to the file on the device. The remote * folder structure is created automatically if necessary. * @param {PushFileOptions} [opts={}] */ async function pushFile(afcService, localPathOrPayload, remotePath, opts = {}) { const { timeoutMs = exports.IO_TIMEOUT_MS, } = opts; const timer = new support_1.timing.Timer().start(); await remoteMkdirp(afcService, path_1.default.dirname(remotePath)); const source = Buffer.isBuffer(localPathOrPayload) ? localPathOrPayload : support_1.fs.createReadStream(localPathOrPayload, { autoClose: true }); const writeStream = await afcService.createWriteStream(remotePath, { autoDestroy: true, }); writeStream.on('finish', writeStream.destroy); let pushError = null; const filePushPromise = new bluebird_1.default((resolve, reject) => { writeStream.on('close', () => { if (pushError) { reject(pushError); } else { resolve(); } }); const onStreamError = (e) => { if (!Buffer.isBuffer(source)) { source.unpipe(writeStream); } logger_1.default.debug(e); pushError = e; }; writeStream.on('error', onStreamError); if (!Buffer.isBuffer(source)) { source.on('error', onStreamError); } }); if (Buffer.isBuffer(source)) { writeStream.write(source); writeStream.end(); } else { source.pipe(writeStream); } await filePushPromise.timeout(Math.max(timeoutMs, 60000)); const fileSize = Buffer.isBuffer(localPathOrPayload) ? localPathOrPayload.length : (await support_1.fs.stat(localPathOrPayload)).size; logger_1.default.debug(`Successfully pushed the file payload (${support_1.util.toReadableSizeString(fileSize)}) ` + `to the remote location '${remotePath}' in ${timer.getDuration().asMilliSeconds.toFixed(0)}ms`); } ; /** * @typedef {Object} PushFolderOptions * * @property {number} [timeoutMs=240000] The maximum timeout to wait until a * single file is copied * @property {boolean} [enableParallelPush=false] Whether to push files in parallel. * This usually gives better performance, but might sometimes be less stable. */ /** * Pushes a folder to a real device * * @param {any} afcService Apple File Client service instance from * 'appium-ios-device' module * @param {string} srcRootPath The full path to the source folder * @param {string} dstRootPath The relative path to the destination folder. The folder * will be deleted if already exists. * @param {PushFolderOptions} opts */ async function pushFolder(afcService, srcRootPath, dstRootPath, opts = {}) { const { timeoutMs = exports.IO_TIMEOUT_MS, enableParallelPush = false } = opts; const timer = new support_1.timing.Timer().start(); const allItems = /** @type {import('path-scurry').Path[]} */ ( /** @type {unknown} */(await support_1.fs.glob('**', { cwd: srcRootPath, withFileTypes: true, }))); logger_1.default.debug(`Successfully scanned the tree structure of '${srcRootPath}'`); // top-level folders go first /** @type {string[]} */ const foldersToPush = allItems .filter((x) => x.isDirectory()) .map((x) => x.relative()) .sort((a, b) => a.split(path_1.default.sep).length - b.split(path_1.default.sep).length); // larger files go first /** @type {string[]} */ const filesToPush = allItems .filter((x) => !x.isDirectory()) .sort((a, b) => (b.size ?? 0) - (a.size ?? 0)) .map((x) => x.relative()); logger_1.default.debug(`Got ${support_1.util.pluralize('folder', foldersToPush.length, true)} and ` + `${support_1.util.pluralize('file', filesToPush.length, true)} to push`); // create the folder structure first try { await afcService.deleteDirectory(dstRootPath); } catch { } await afcService.createDirectory(dstRootPath); for (const relativeFolderPath of foldersToPush) { // createDirectory does not accept folder names ending with a path separator const absoluteFolderPath = lodash_1.default.trimEnd(path_1.default.join(dstRootPath, relativeFolderPath), path_1.default.sep); if (absoluteFolderPath) { await afcService.createDirectory(absoluteFolderPath); } } // do not forget about the root folder logger_1.default.debug(`Successfully created the remote folder structure ` + `(${support_1.util.pluralize('item', foldersToPush.length + 1, true)})`); const _pushFile = async (/** @type {string} */ relativePath) => { const absoluteSourcePath = path_1.default.join(srcRootPath, relativePath); const readStream = support_1.fs.createReadStream(absoluteSourcePath, { autoClose: true }); const absoluteDestinationPath = path_1.default.join(dstRootPath, relativePath); const writeStream = await afcService.createWriteStream(absoluteDestinationPath, { autoDestroy: true, }); writeStream.on('finish', writeStream.destroy); let pushError = null; const filePushPromise = new bluebird_1.default((resolve, reject) => { writeStream.on('close', () => { if (pushError) { reject(pushError); } else { resolve(); } }); const onStreamError = (e) => { readStream.unpipe(writeStream); logger_1.default.debug(e); pushError = e; }; writeStream.on('error', onStreamError); readStream.on('error', onStreamError); }); readStream.pipe(writeStream); await filePushPromise.timeout(Math.max(timeoutMs - timer.getDuration().asMilliSeconds, 60000)); }; if (enableParallelPush) { logger_1.default.debug(`Proceeding to parallel files push (max ${MAX_IO_CHUNK_SIZE} writers)`); const pushPromises = []; for (const relativeFilePath of filesToPush) { pushPromises.push(bluebird_1.default.resolve(_pushFile(relativeFilePath))); // keep the push queue filled if (pushPromises.length >= MAX_IO_CHUNK_SIZE) { await bluebird_1.default.any(pushPromises); const elapsedMs = timer.getDuration().asMilliSeconds; if (elapsedMs > timeoutMs) { throw new bluebird_1.TimeoutError(`Timed out after ${elapsedMs} ms`); } } lodash_1.default.remove(pushPromises, (p) => p.isFulfilled()); } if (!lodash_1.default.isEmpty(pushPromises)) { // handle the rest of push promises await bluebird_1.default.all(pushPromises).timeout(Math.max(timeoutMs - timer.getDuration().asMilliSeconds, 60000)); } } else { logger_1.default.debug(`Proceeding to serial files push`); for (const relativeFilePath of filesToPush) { await _pushFile(relativeFilePath); const elapsedMs = timer.getDuration().asMilliSeconds; if (elapsedMs > timeoutMs) { throw new bluebird_1.TimeoutError(`Timed out after ${elapsedMs} ms`); } } } logger_1.default.debug(`Successfully pushed ${support_1.util.pluralize('folder', foldersToPush.length, true)} ` + `and ${support_1.util.pluralize('file', filesToPush.length, true)} ` + `within ${timer.getDuration().asMilliSeconds.toFixed(0)}ms`); } //# sourceMappingURL=ios-fs-helpers.js.map