gst-atom-xcuitest-driver
Version:
ATOM driver for iOS using XCUITest for backend
318 lines (284 loc) • 10.8 kB
JavaScript
/* eslint-disable promise/prefer-await-to-callbacks */
import { fs, timing } from 'appium-support';
import path from 'path';
import { services, utilities } from 'gst-atom-ios-device';
import B from 'bluebird';
import log from './logger';
import _ from 'lodash';
import { exec } from 'teen_process';
import fetch from 'node-fetch';
const APPLICATION_INSTALLED_NOTIFICATION = 'com.apple.mobile.application_installed';
const INSTALLATION_STAGING_DIR = 'PublicStaging';
const DEFAULT_ITEM_PUSH_TIMEOUT = 30 * 1000;
const APPLICATION_NOTIFICATION_TIMEOUT = 30 * 1000;
const IOS_DEPLOY = 'ios-deploy';
class IOSDeploy {
constructor (opts) {
this.udid = opts.udid;
this.opts = opts;
}
async remove (bundleId) {
// If we are using remote device in device farm
// Call API to remove application
if(this.opts.webDriverAgentUrl){
const body = {
serial: this.opts.udid,
bundleId: bundleId
};
log.info(`[${this.opts.udid}] Calling Device Farm to uninstall ${bundleId}`);
let response = await fetch(this.opts.dfIOSUninstallApi, {
method: 'post',
body: JSON.stringify(body),
headers: { 'Content-Type': 'application/json',
'Authorization': 'Bearer ' + this.opts.dfToken
},
timeout: 3600000
}).catch(err => {
log.error(`[${this.opts.udid}] ${err.message}`);
log.errorAndThrow(err);
});
let data = await response.json();
if(!data.success) {
log.errorAndThrow(`[${this.opts.udid}] ${data.description}`);
}
log.info(`[${this.opts.udid}][${bundleId}] Uninstallation successfully`);
return;
}
var options = {
udid: this.udid,
usbmuxdRemoteHost: this.opts.usbmuxdRemoteHost,
usbmuxdRemotePort: this.opts.usbmuxdRemotePort
};
const service = await services.startInstallationProxyService(options);
try {
await service.uninstallApplication(bundleId);
} finally {
service.close();
}
}
async removeApp (bundleId) {
await this.remove(bundleId);
}
// NhuNH - install application through DF
async installToDeviceFarm(){
const body = { type: 'iOS',
serial: this.opts.udid,
url: this.opts.appUrl
};
log.info(`[${this.opts.udid}] Calling Device Farm to install iOS application at ${this.opts.appUrl}`);
let response = await fetch(this.opts.dfIOSInstallApi, {
method: 'post',
body: JSON.stringify(body),
headers: { 'Content-Type': 'application/json',
'Authorization': 'Bearer ' + this.opts.dfToken
},
timeout: 3600000
}).catch(err => {
log.error(`[${this.opts.udid}] ${err.message}`);
log.errorAndThrow(err);
});
let data = await response.json();
if(!data.success) {
log.errorAndThrow(`[${this.opts.udid}] ${data.description}`);
}
log.info(`[${this.opts.udid}] Installation successfully (${this.opts.appUrl}) `);
}
async install (app, timeout) {
if (this.opts.webDriverAgentUrl){
await this.installToDeviceFarm()
return;
}
const timer = new timing.Timer().start();
try {
const bundlePathOnPhone = await this.pushAppBundle(app, timeout);
await this.installApplication(bundlePathOnPhone);
} catch (err) {
log.warn(`Error installing app: ${err.message}`);
log.warn(`Falling back to '${IOS_DEPLOY}' usage`);
try {
await fs.which(IOS_DEPLOY);
} catch (err1) {
throw new Error(`Could not install '${app}':\n` +
` - ${err.message}\n` +
` - '${IOS_DEPLOY}' utility has not been found in PATH. Is it installed?`);
}
try {
await exec(IOS_DEPLOY, [
'--id', this.udid,
'--bundle', app,
]);
} catch (err1) {
throw new Error(`Could not install '${app}':\n` +
` - ${err.message}\n` +
` - ${err1.stderr || err1.stdout || err1.message}`);
}
}
log.info(`App installation succeeded after ${timer.getDuration().asMilliSeconds.toFixed(0)}ms`);
}
async installApplication (bundlePathOnPhone) {
var options = {
udid: this.udid,
usbmuxdRemoteHost: this.opts.usbmuxdRemoteHost,
usbmuxdRemotePort: this.opts.usbmuxdRemotePort
};
const notificationService = await services.startNotificationProxyService(options);
const installationService = await services.startInstallationProxyService(options);
const appInstalledNotification = new B((resolve) => {
notificationService.observeNotification(APPLICATION_INSTALLED_NOTIFICATION, {notification: resolve});
});
try {
await installationService.installApplication(bundlePathOnPhone, {PackageType: 'Developer'});
try {
await appInstalledNotification.timeout(APPLICATION_NOTIFICATION_TIMEOUT, `Could not get the application installed notification within ${APPLICATION_NOTIFICATION_TIMEOUT}ms but we will continue`);
} catch (e) {
log.warn(`Failed to receive the notification. Error: ${e.message}`);
}
} finally {
installationService.close();
notificationService.close();
}
}
async pushAppBundle (app, timeout = DEFAULT_ITEM_PUSH_TIMEOUT) {
const timer = new timing.Timer().start();
var options = {
udid: this.udid,
usbmuxdRemoteHost: this.opts.usbmuxdRemoteHost,
usbmuxdRemotePort: this.opts.usbmuxdRemotePort
};
const afcService = await services.startAfcService(options);
// We are pushing serially due to this https://github.com/appium/appium/issues/13115. There is nothing else we can do besides this
try {
const bundlePathOnPhone = await this.createAppPath(afcService, app);
await fs.walkDir(app, true, async (itemPath, isDir) => {
const pathOnPhone = path.join(bundlePathOnPhone, path.relative(app, itemPath));
if (isDir) {
await afcService.createDirectory(pathOnPhone);
} else {
const readStream = fs.createReadStream(itemPath, {autoClose: true});
const writeStream = await afcService.createWriteStream(pathOnPhone, {autoDestroy: true});
writeStream.on('finish', writeStream.destroy);
let pushError = null;
const itemPushWait = new B((resolve, reject) => {
writeStream.on('close', () => {
if (pushError) {
reject(pushError);
} else {
resolve();
}
});
const onStreamError = (e) => {
readStream.unpipe(writeStream);
log.debug(e);
pushError = e;
};
writeStream.on('error', onStreamError);
readStream.on('error', onStreamError);
});
readStream.pipe(writeStream);
await itemPushWait.timeout(timeout,
`Could not push '${itemPath}' within the timeout of ${timeout}ms. ` +
`Consider increasing the value of 'appPushTimeout' capability.`);
}
});
log.debug(`Pushed the app files successfully after ${timer.getDuration().asMilliSeconds.toFixed(0)}ms`);
return bundlePathOnPhone;
} finally {
afcService.close();
}
}
async createAppPath (afcService, localAppPath) {
const basename = path.basename(localAppPath);
const relativePath = path.join(INSTALLATION_STAGING_DIR, basename);
try {
await afcService.deleteDirectory(relativePath);
} catch (ign) {}
await afcService.createDirectory(relativePath);
return relativePath;
}
async installApp (app, timeout) {
await this.install(app, timeout);
}
/**
* Return an application object if test app has 'bundleid'.
* The target bundleid can be User and System apps.
* @param {string} bundleId The bundleId to ensure it is installed
* @return {boolean} Returns True if the bundleid exists in the result of 'listApplications' like:
* { "com.apple.Preferences":{
* "UIRequiredDeviceCapabilities":["arm64"],
* "UIRequiresFullScreen":true,
* "CFBundleInfoDictionaryVersion":"6.0",
* "Entitlements":
* {"com.apple.frontboard.delete-application-snapshots":true,..
*/
async isAppInstalled (bundleId) {
// If using remote device on device farm,
// call device farm api to check an application is installed
if(this.opts.webDriverAgentUrl) {
const body = {
serial: this.opts.udid,
bundleId: bundleId
};
log.info(`[${this.opts.udid}] Calling Device Farm to check the application with bundle id ${this.opts.appUrl} is installed`);
let response = await fetch(this.opts.dfAppInstalledApi, {
method: 'post',
body: JSON.stringify(body),
headers: { 'Content-Type': 'application/json',
'Authorization': 'Bearer ' + this.opts.dfToken
},
timeout: 60000
}).catch(err => {
log.error(`[${this.opts.udid}] ${err.message}`);
log.errorAndThrow(err);
});
let data = await response.json();
log.info(`[${this.opts.udid}] Application ${bundleId} is installed: ${data.isInstalled}`);
return data.isInstalled;
}
var options = {
udid: this.udid,
usbmuxdRemoteHost: this.opts.usbmuxdRemoteHost,
usbmuxdRemotePort: this.opts.usbmuxdRemotePort
};
const service = await services.startInstallationProxyService(options);
try {
const applications = await service.lookupApplications({ bundleIds: bundleId });
return !!applications[bundleId];
} finally {
service.close();
}
}
/**
* @param {string} bundleName The name of CFBundleName in Info.plist
*
* @returns {Array<string>} A list of User level apps' bundle ids which has
* 'CFBundleName' attribute as 'bundleName'.
*/
async getUserInstalledBundleIdsByBundleName (bundleName) {
var options = {
udid: this.udid,
usbmuxdRemoteHost: this.opts.usbmuxdRemoteHost,
usbmuxdRemotePort: this.opts.usbmuxdRemotePort
};
const service = await services.startInstallationProxyService(options);
try {
const applications = await service.listApplications({applicationType: 'User'});
return _.reduce(applications, (acc, {CFBundleName}, key) => {
if (CFBundleName === bundleName) {
acc.push(key);
}
return acc;
}, []);
} finally {
service.close();
}
}
async getPlatformVersion () {
var options = {
udid: this.udid,
usbmuxdRemoteHost: this.opts.usbmuxdRemoteHost,
usbmuxdRemotePort: this.opts.usbmuxdRemotePort
};
return await utilities.getOSVersion(null, options);
}
}
export default IOSDeploy;