appium-xcuitest-driver
Version:
Appium driver for iOS using XCUITest for backend
360 lines • 15.4 kB
JavaScript
;
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