Skip to content

Instantly share code, notes, and snippets.

@paradite
Created July 28, 2020 07:03
Show Gist options
  • Save paradite/1569c4012dd1bdb3d71213b536bb5a05 to your computer and use it in GitHub Desktop.
Save paradite/1569c4012dd1bdb3d71213b536bb5a05 to your computer and use it in GitHub Desktop.
'use strict';
const fs = require('fs');
const url = require('url');
const path = require('path');
const iOSUtils = require('ios-utils');
const EventEmitter = require('events');
const childProcess = require('child_process');
const _ = require('./helper');
const pkg = require('../package');
const XCProxy = require('./proxy');
const logger = require('./logger');
const XCTestWD = require('./xctestwd');
const {
detectPort
} = _;
const TEST_URL = pkg.site;
const projectPath = XCTestWD.projectPath;
const SERVER_URL_REG = XCTestWD.SERVER_URL_REG;
const simulatorLogFlag = XCTestWD.simulatorLogFlag;
class XCTest extends EventEmitter {
constructor(options) {
super();
this.proxy = null;
this.capabilities = null;
this.sessionId = null;
this.device = null;
this.deviceLogProc = null;
this.runnerProc = null;
this.iproxyProc = null;
Object.assign(this, {
proxyHost: '127.0.0.1',
proxyPort: 8001,
urlBase: 'wd/hub'
}, options || {});
this.init();
}
init() {
this.checkProjectPath();
process.on('uncaughtException', (e) => {
logger.error(`Uncaught Exception: ${e.stack}`);
this.stop();
process.exit(1);
});
process.on('exit', () => {
this.stop();
});
}
checkProjectPath() {
if (_.isExistedDir(projectPath)) {
logger.debug(`project path: ${projectPath}`);
} else {
logger.error('project path not found');
}
}
configUrl(str) {
const urlObj = url.parse(str);
this.proxyHost = urlObj.hostname;
this.proxyPort = urlObj.port;
}
initProxy() {
this.proxy = new XCProxy({
proxyHost: this.proxyHost,
proxyPort: this.proxyPort,
urlBase: this.urlBase
});
}
* startSimLog() {
const logPath = yield this.startBootstrap(false);
if (this.proxyHost && this.proxyPort) {
return;
}
const logDir = path.resolve(logPath);
return _.retry(() => {
return new Promise((resolve, reject) => {
let logTxtFile = path.join(logDir, '..', 'StandardOutputAndStandardError.txt');
logTxtFile = logDir.replace(/(\s)/, '\\ ');
logger.info(`Read simulator log at: ${logTxtFile}`);
if (!_.isExistedFile(logTxtFile)) {
return reject();
}
let args = `-f -n 0 ${logTxtFile}`.split(' ');
var proc = childProcess.spawn('tail', args, {});
this.deviceLogProc = proc;
proc.stderr.setEncoding('utf8');
proc.stdout.setEncoding('utf8');
proc.stdout.on('data', data => {
// avoid logout long data such as bitmap
if (data.length <= 300 && logger.debugMode) {
logger.debug(data);
}
let match = SERVER_URL_REG.exec(data);
if (match) {
const url = match[1];
if (url.startsWith('http://')) {
this.configUrl(url);
resolve();
}
}
});
proc.stderr.on('data', data => {
logger.debug(data);
});
proc.stdout.on('error', (err) => {
logger.warn(`simulator log process error with ${err}`);
});
proc.on('exit', (code, signal) => {
logger.warn(`simulator log process exit with code: ${code}, signal: ${signal}`);
reject();
});
});
}, 1000, Infinity);
}
* startDeviceLog() {
yield this.startBootstrap(true);
if (this.proxyHost && this.proxyPort) {
return;
}
var proc = childProcess.spawn(iOSUtils.devicelog.binPath, [this.device.deviceId], {});
this.deviceLogProc = proc;
proc.stderr.setEncoding('utf8');
proc.stdout.setEncoding('utf8');
return new Promise((resolve, reject) => {
proc.stdout.on('data', data => {
// avoid logout long data such as bitmap
if (data.length <= 300 && logger.debugMode) {
logger.debug(data);
}
let match = SERVER_URL_REG.exec(data);
if (match) {
const url = match[1];
if (url.startsWith('http://')) {
this.configUrl(url);
resolve();
}
}
});
proc.stderr.on('data', data => {
logger.debug(data);
});
proc.stdout.on('error', (err) => {
logger.warn(`devicelog error with ${err}`);
});
proc.on('exit', (code, signal) => {
logger.warn(`devicelog exit with code: ${code}, signal: ${signal}`);
reject();
});
});
}
* startBootstrap(isDevice) {
return new Promise((resolve, reject) => {
logger.info(`XCTestWD version: ${XCTestWD.version}`);
var args = `clean test -project ${XCTestWD.projectPath} -scheme XCTestWDUITests -destination id=${this.device.deviceId} XCTESTWD_PORT=${this.proxyPort}`.split(' ');
// check potential optimization provided by .xctestrun file, which allows xctestwd can be executed concurrently on multiple devices
let xctestrun_path = path.join(__dirname, '..', 'XCTestWD', 'build', 'Build', 'Products');
let xctestrun = fs.readdirSync(xctestrun_path).filter(fn => fn.match(isDevice ? '.*device.*\.xctestrun' : '.*simulator.*\.xctestrun')).shift();
if (xctestrun) {
args = `test-without-building -xctestrun ${xctestrun_path}/${xctestrun} -destination id=${this.device.deviceId} XCTESTWD_PORT=${this.proxyPort}`.split(' ');
}
var env = _.merge({}, process.env, {
XCTESTWD_PORT: this.proxyPort
});
var proc = childProcess.spawn('xcodebuild', args, {
env: env
});
this.runnerProc = proc;
proc.stderr.setEncoding('utf8');
proc.stdout.setEncoding('utf8');
proc.stdout.on('data', data => {
logger.debug(data);
if (xctestrun) {
let match = SERVER_URL_REG.exec(data);
if (match) {
const url = match[1];
if (url.startsWith('http://')) {
logger.debug('hitted for xctestrun mode');
this.configUrl(url);
resolve();
}
}
}
});
proc.stderr.on('data', data => {
if (data.length > 1000) {
logger.warn(data.slice(0, 1000) + '...');
} else {
logger.warn(data);
}
if (!xctestrun) {
if (~data.indexOf(simulatorLogFlag)) {
const list = data.split(simulatorLogFlag);
const res = list[1].trim();
logger.debug('hitted for default mode');
resolve(res);
} else {
logger.debug(`please check project: ${projectPath}`);
}
}
});
proc.stdout.on('error', (err) => {
logger.warn(`xctest client error with ${err}`);
logger.debug(`please check project: ${projectPath}`);
});
proc.on('exit', (code, signal) => {
this.stop();
logger.warn(`xctest client exit with code: ${code}, signal: ${signal}`);
});
});
}
* startIproxy() {
let args = [`${this.proxyPort}:${this.proxyPort}`,'-u',this.device.deviceId];
const IOS_USBMUXD_IPROXY = 'iproxy';
const binPath = yield _.exec(`which ${IOS_USBMUXD_IPROXY}`);
var proc = childProcess.spawn(binPath, args);
this.iproxyProc = proc;
proc.stderr.setEncoding('utf8');
proc.stdout.setEncoding('utf8');
proc.stdout.on('data', () => {
});
proc.stderr.on('data', (data) => {
if (data.length > 1000) {
logger.warn(data.slice(0, 1000) + '...');
} else {
logger.warn(data);
}
});
proc.stdout.on('error', (err) => {
logger.warn(`${IOS_USBMUXD_IPROXY} error with ${err}`);
});
proc.on('exit', (code, signal) => {
logger.warn(`${IOS_USBMUXD_IPROXY} exit with code: ${code}, signal: ${signal}`);
});
}
* start(caps) {
try {
this.proxyPort = yield detectPort(this.proxyPort);
this.capabilities = caps;
const xcodeVersion = yield iOSUtils.getXcodeVersion();
logger.debug(`xcode version: ${xcodeVersion}`);
var deviceInfo = iOSUtils.getDeviceInfo(this.device.deviceId);
if (deviceInfo.isRealIOS) {
yield this.startDeviceLog();
yield this.startIproxy();
} else {
yield this.startSimLog();
}
logger.info(`${pkg.name} start with port: ${this.proxyPort}`);
this.initProxy();
if (caps.desiredCapabilities.browserName === 'Safari') {
var promise = this.proxy.send(`/${this.urlBase}/session`, 'POST', {
desiredCapabilities: {
bundleId: 'com.apple.mobilesafari'
}
});
return yield Promise.all([this.device.openURL(TEST_URL), promise]);
} else {
return yield this.proxy.send(`/${this.urlBase}/session`, 'POST', caps);
}
} catch (err) {
logger.debug(`Fail to start xctest: ${err}`);
this.stop();
throw err;
}
}
stop() {
if (this.deviceLogProc) {
logger.debug(`killing deviceLogProc pid: ${this.deviceLogProc.pid}`);
this.deviceLogProc.kill('SIGKILL');
this.deviceLogProc = null;
}
if (this.runnerProc) {
logger.debug(`killing runnerProc pid: ${this.runnerProc.pid}`);
this.runnerProc.kill('SIGKILL');
this.runnerProc = null;
}
if (this.iproxyProc) {
logger.debug(`killing iproxyProc pid: ${this.iproxyProc.pid}`);
this.iproxyProc.kill('SIGKILL');
this.iproxyProc = null;
}
}
sendCommand(url, method, body) {
return this.proxy.send(url, method, body);
}
}
module.exports = XCTest;
module.exports.XCTestWD = XCTestWD;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment