зеркало из https://github.com/xamarin/appium.old.git
520 строки
18 KiB
JavaScript
Executable File
520 строки
18 KiB
JavaScript
Executable File
import _ from 'lodash';
|
|
import log from './logger';
|
|
import { getBuildInfo, updateBuildInfo } from './config';
|
|
import { BaseDriver, errors, isSessionCommand } from 'appium-base-driver';
|
|
import { FakeDriver } from 'appium-fake-driver';
|
|
import { AndroidDriver } from 'appium-android-driver';
|
|
import { IosDriver } from 'appium-ios-driver';
|
|
import { AndroidUiautomator2Driver } from 'appium-uiautomator2-driver';
|
|
import { SelendroidDriver } from 'appium-selendroid-driver';
|
|
import { XCUITestDriver } from 'appium-xcuitest-driver';
|
|
import { YouiEngineDriver } from 'appium-youiengine-driver';
|
|
import { WindowsDriver } from 'appium-windows-driver';
|
|
import { MacDriver } from 'appium-mac-driver';
|
|
import { EspressoDriver } from 'appium-espresso-driver';
|
|
import { TizenDriver } from 'appium-tizen-driver';
|
|
import B from 'bluebird';
|
|
import AsyncLock from 'async-lock';
|
|
import { inspectObject, parseCapsForInnerDriver, getPackageVersion } from './utils';
|
|
import semver from 'semver';
|
|
|
|
const PLATFORMS = {
|
|
FAKE: 'fake',
|
|
ANDROID: 'android',
|
|
IOS: 'ios',
|
|
WINDOWS: 'windows',
|
|
MAC: 'mac',
|
|
TIZEN: 'tizen',
|
|
};
|
|
|
|
const AUTOMATION_NAMES = {
|
|
APPIUM: 'Appium',
|
|
SELENDROID: 'Selendroid',
|
|
UIAUTOMATOR2: 'UiAutomator2',
|
|
UIAUTOMATOR1: 'UiAutomator1',
|
|
XCUITEST: 'XCUITest',
|
|
YOUIENGINE: 'YouiEngine',
|
|
ESPRESSO: 'Espresso',
|
|
TIZEN: 'Tizen',
|
|
FAKE: 'Fake',
|
|
INSTRUMENTS: 'Instruments',
|
|
};
|
|
const DRIVER_MAP = {
|
|
SelendroidDriver: {
|
|
driverClass: SelendroidDriver,
|
|
automationName: AUTOMATION_NAMES.SELENDROID,
|
|
version: getPackageVersion('appium-selendroid-driver'),
|
|
},
|
|
AndroidUiautomator2Driver: {
|
|
driverClass: AndroidUiautomator2Driver,
|
|
automationName: AUTOMATION_NAMES.UIAUTOMATOR2,
|
|
version: getPackageVersion('appium-uiautomator2-driver'),
|
|
},
|
|
XCUITestDriver: {
|
|
driverClass: XCUITestDriver,
|
|
automationName: AUTOMATION_NAMES.XCUITEST,
|
|
version: getPackageVersion('appium-xcuitest-driver'),
|
|
},
|
|
YouiEngineDriver: {
|
|
driverClass: YouiEngineDriver,
|
|
automationName: AUTOMATION_NAMES.YOUIENGINE,
|
|
version: getPackageVersion('appium-youiengine-driver'),
|
|
},
|
|
FakeDriver: {
|
|
driverClass: FakeDriver,
|
|
version: getPackageVersion('appium-fake-driver'),
|
|
},
|
|
AndroidDriver: {
|
|
driverClass: AndroidDriver,
|
|
automationName: AUTOMATION_NAMES.UIAUTOMATOR1,
|
|
version: getPackageVersion('appium-android-driver'),
|
|
},
|
|
IosDriver: {
|
|
driverClass: IosDriver,
|
|
automationName: AUTOMATION_NAMES.INSTRUMENTS,
|
|
version: getPackageVersion('appium-ios-driver'),
|
|
},
|
|
WindowsDriver: {
|
|
driverClass: WindowsDriver,
|
|
version: getPackageVersion('appium-windows-driver'),
|
|
},
|
|
MacDriver: {
|
|
driverClass: MacDriver,
|
|
version: getPackageVersion('appium-mac-driver'),
|
|
},
|
|
EspressoDriver: {
|
|
driverClass: EspressoDriver,
|
|
automationName: AUTOMATION_NAMES.ESPRESSO,
|
|
version: getPackageVersion('appium-espresso-driver'),
|
|
},
|
|
TizenDriver: {
|
|
driverClass: TizenDriver,
|
|
automationName: AUTOMATION_NAMES.TIZEN,
|
|
version: getPackageVersion('appium-tizen-driver'),
|
|
},
|
|
};
|
|
|
|
const PLATFORMS_MAP = {
|
|
[PLATFORMS.FAKE]: () => FakeDriver,
|
|
[PLATFORMS.ANDROID]: (caps) => {
|
|
const platformVersion = semver.valid(semver.coerce(caps.platformVersion));
|
|
log.warn(`DeprecationWarning: 'automationName' capability was not provided. ` +
|
|
`Future versions of Appium will require 'automationName' capability to be set for Android sessions.`);
|
|
log.info(`Setting automation to '${AUTOMATION_NAMES.UIAUTOMATOR1}'. `);
|
|
if (platformVersion && semver.satisfies(platformVersion, '>=6.0.0')) {
|
|
log.warn(`Consider setting 'automationName' capability to '${AUTOMATION_NAMES.UIAUTOMATOR2}' ` +
|
|
'on Android >= 6, since UIAutomator1 framework ' +
|
|
'is not maintained anymore by the OS vendor.');
|
|
}
|
|
|
|
return AndroidDriver;
|
|
},
|
|
[PLATFORMS.IOS]: (caps) => {
|
|
const platformVersion = semver.valid(semver.coerce(caps.platformVersion));
|
|
log.warn(`DeprecationWarning: 'automationName' capability was not provided. ` +
|
|
`Future versions of Appium will require 'automationName' capability to be set for iOS sessions.`);
|
|
if (platformVersion && semver.satisfies(platformVersion, '>=10.0.0')) {
|
|
log.info("Requested iOS support with version >= 10, " +
|
|
`using '${AUTOMATION_NAMES.XCUITEST}' ` +
|
|
"driver instead of UIAutomation-based driver, since the " +
|
|
"latter is unsupported on iOS 10 and up.");
|
|
return XCUITestDriver;
|
|
}
|
|
|
|
return IosDriver;
|
|
},
|
|
[PLATFORMS.WINDOWS]: () => WindowsDriver,
|
|
[PLATFORMS.MAC]: () => MacDriver,
|
|
[PLATFORMS.TIZEN]: () => TizenDriver,
|
|
};
|
|
|
|
const desiredCapabilityConstraints = {
|
|
automationName: {
|
|
presence: false,
|
|
isString: true,
|
|
inclusionCaseInsensitive: _.values(AUTOMATION_NAMES),
|
|
},
|
|
platformName: {
|
|
presence: true,
|
|
isString: true,
|
|
inclusionCaseInsensitive: _.keys(PLATFORMS_MAP),
|
|
},
|
|
};
|
|
|
|
const sessionsListGuard = new AsyncLock();
|
|
const pendingDriversGuard = new AsyncLock();
|
|
|
|
function ensureRightCaps (opts) {
|
|
if(_.isObject(opts)) {
|
|
if (!process.env.XTC_DEVICE_SERIAL) {
|
|
throw new Error(`XTC_DEVICE_SERIAL is not set`);
|
|
}
|
|
opts.udid = process.env.XTC_DEVICE_SERIAL;
|
|
|
|
if (!process.env.XTC_APP_ID) {
|
|
throw new Error(`XTC_APP_ID is not set`);
|
|
}
|
|
opts.bundleId = process.env.XTC_APP_ID; // todo: consider removing from TestCloudSpoofer
|
|
|
|
if (_.isString(process.env.XTC_FORCE_APP_PATH)) {
|
|
opts.app = process.env.XTC_FORCE_APP_PATH;
|
|
}
|
|
}
|
|
}
|
|
|
|
function ensureRightNSedApp (array) {
|
|
if (_.isString(process.env.XTC_FORCE_APP_PATH) && _.isArray(array)) {
|
|
array.forEach(e => { e['appium:app'] = process.env.XTC_FORCE_APP_PATH });
|
|
}
|
|
}
|
|
|
|
class AppiumDriver extends BaseDriver {
|
|
constructor (args) {
|
|
super();
|
|
|
|
this.desiredCapConstraints = desiredCapabilityConstraints;
|
|
|
|
// the main Appium Driver has no new command timeout
|
|
this.newCommandTimeoutMs = 0;
|
|
|
|
this.args = Object.assign({}, args);
|
|
|
|
// Access to sessions list must be guarded with a Semaphore, because
|
|
// it might be changed by other async calls at any time
|
|
// It is not recommended to access this property directly from the outside
|
|
this.sessions = {};
|
|
|
|
// Access to pending drivers list must be guarded with a Semaphore, because
|
|
// it might be changed by other async calls at any time
|
|
// It is not recommended to access this property directly from the outside
|
|
this.pendingDrivers = {};
|
|
|
|
// allow this to happen in the background, so no `await`
|
|
updateBuildInfo();
|
|
}
|
|
|
|
/**
|
|
* Cancel commands queueing for the umbrella Appium driver
|
|
*/
|
|
get isCommandsQueueEnabled () {
|
|
return false;
|
|
}
|
|
|
|
sessionExists (sessionId) {
|
|
const dstSession = this.sessions[sessionId];
|
|
return dstSession && dstSession.sessionId !== null;
|
|
}
|
|
|
|
driverForSession (sessionId) {
|
|
return this.sessions[sessionId];
|
|
}
|
|
|
|
getDriverForCaps (caps) {
|
|
const force_automation_name = process.env.XTC_FORCE_AUTOMATIONNAME;
|
|
if (_.isString(force_automation_name)) {
|
|
log.info(`Forcing automation name to '${force_automation_name}'`);
|
|
caps.automationName = force_automation_name;
|
|
}
|
|
|
|
const fallback_from_espresso_driver = process.env.FALLBACK_FROM_ESPRESSO_DRIVER;
|
|
if (_.isString(fallback_from_espresso_driver) && caps.automationName === AUTOMATION_NAMES.ESPRESSO) {
|
|
log.info(`Forcing automation name to '${fallback_from_espresso_driver}'`);
|
|
caps.automationName = fallback_from_espresso_driver;
|
|
}
|
|
|
|
const fallback_from_uiautomation2_driver = process.env.FALLBACK_FROM_UIAUTOMATOR2_DRIVER;
|
|
if (_.isString(fallback_from_uiautomation2_driver) && caps.automationName === AUTOMATION_NAMES.UIAUTOMATOR2) {
|
|
log.info(`Forcing automation name to '${fallback_from_uiautomation2_driver}'`);
|
|
caps.automationName = fallback_from_uiautomation2_driver;
|
|
}
|
|
|
|
// we don't necessarily have an `automationName` capability,
|
|
if (_.isString(caps.automationName)) {
|
|
for (const {automationName, driverClass} of _.values(DRIVER_MAP)) {
|
|
if (_.toLower(automationName) === caps.automationName.toLowerCase()) {
|
|
return driverClass;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!_.isString(caps.platformName)) {
|
|
throw new Error("You must include a platformName capability");
|
|
}
|
|
|
|
const platformName = caps.platformName.toLowerCase();
|
|
|
|
const driverSelector = PLATFORMS_MAP[platformName];
|
|
if (driverSelector) {
|
|
return driverSelector(caps);
|
|
}
|
|
|
|
const msg = _.isString(caps.automationName)
|
|
? `Could not find a driver for automationName '${caps.automationName}' and platformName ` +
|
|
`'${caps.platformName}'.`
|
|
: `Could not find a driver for platformName '${caps.platformName}'.`;
|
|
throw new Error(`${msg} Please check your desired capabilities.`);
|
|
}
|
|
|
|
getDriverVersion (driver) {
|
|
const {version} = DRIVER_MAP[driver.name] || {};
|
|
if (version) {
|
|
return version;
|
|
}
|
|
log.warn(`Unable to get version of driver '${driver.name}'`);
|
|
}
|
|
|
|
async getStatus () { // eslint-disable-line require-await
|
|
return {
|
|
build: _.clone(getBuildInfo()),
|
|
};
|
|
}
|
|
|
|
async getSessions () {
|
|
const sessions = await sessionsListGuard.acquire(AppiumDriver.name, () => this.sessions);
|
|
return _.toPairs(sessions)
|
|
.map(([id, driver]) => {
|
|
return {id, capabilities: driver.caps};
|
|
});
|
|
}
|
|
|
|
printNewSessionAnnouncement (driver, caps) {
|
|
const driverVersion = this.getDriverVersion(driver);
|
|
const introString = driverVersion
|
|
? `Creating new ${driver.name} (v${driverVersion}) session`
|
|
: `Creating new ${driver.name} session`;
|
|
log.info(introString);
|
|
log.info('Capabilities:');
|
|
inspectObject(caps);
|
|
}
|
|
|
|
/**
|
|
* Create a new session
|
|
* @param {Object} jsonwpCaps JSONWP formatted desired capabilities
|
|
* @param {Object} reqCaps Required capabilities (JSONWP standard)
|
|
* @param {Object} w3cCapabilities W3C capabilities
|
|
* @return {Array} Unique session ID and capabilities
|
|
*/
|
|
async createSession (jsonwpCaps, reqCaps, w3cCapabilities) {
|
|
const {defaultCapabilities} = this.args;
|
|
let protocol;
|
|
let innerSessionId, dCaps;
|
|
|
|
ensureRightCaps(jsonwpCaps);
|
|
ensureRightCaps(w3cCapabilities.desiredCapabilities);
|
|
ensureRightNSedApp(w3cCapabilities.firstMatch);
|
|
|
|
|
|
try {
|
|
// Parse the caps into a format that the InnerDriver will accept
|
|
const parsedCaps = parseCapsForInnerDriver(
|
|
jsonwpCaps,
|
|
w3cCapabilities,
|
|
this.desiredCapConstraints,
|
|
defaultCapabilities
|
|
);
|
|
|
|
let {desiredCaps, processedJsonwpCapabilities, processedW3CCapabilities, error} = parsedCaps;
|
|
protocol = parsedCaps.protocol;
|
|
|
|
// If the parsing of the caps produced an error, throw it in here
|
|
if (error) {
|
|
throw error;
|
|
}
|
|
|
|
const InnerDriver = this.getDriverForCaps(desiredCaps);
|
|
this.printNewSessionAnnouncement(InnerDriver, desiredCaps);
|
|
|
|
if (this.args.sessionOverride) {
|
|
const sessionIdsToDelete = await sessionsListGuard.acquire(AppiumDriver.name, () => _.keys(this.sessions));
|
|
if (sessionIdsToDelete.length) {
|
|
log.info(`Session override is on. Deleting other ${sessionIdsToDelete.length} active session${sessionIdsToDelete.length ? '' : 's'}.`);
|
|
try {
|
|
await B.map(sessionIdsToDelete, (id) => this.deleteSession(id));
|
|
} catch (ign) {}
|
|
}
|
|
}
|
|
|
|
let runningDriversData, otherPendingDriversData;
|
|
const d = new InnerDriver(this.args);
|
|
if (this.args.relaxedSecurityEnabled) {
|
|
log.info(`Applying relaxed security to '${InnerDriver.name}' as per server command line argument`);
|
|
d.relaxedSecurityEnabled = true;
|
|
}
|
|
// This assignment is required for correct web sockets functionality inside the driver
|
|
d.server = this.server;
|
|
try {
|
|
runningDriversData = await this.curSessionDataForDriver(InnerDriver);
|
|
} catch (e) {
|
|
throw new errors.SessionNotCreatedError(e.message);
|
|
}
|
|
await pendingDriversGuard.acquire(AppiumDriver.name, () => {
|
|
this.pendingDrivers[InnerDriver.name] = this.pendingDrivers[InnerDriver.name] || [];
|
|
otherPendingDriversData = this.pendingDrivers[InnerDriver.name].map((drv) => drv.driverData);
|
|
this.pendingDrivers[InnerDriver.name].push(d);
|
|
});
|
|
|
|
try {
|
|
[innerSessionId, dCaps] = await d.createSession(
|
|
processedJsonwpCapabilities,
|
|
reqCaps,
|
|
processedW3CCapabilities,
|
|
[...runningDriversData, ...otherPendingDriversData]
|
|
);
|
|
protocol = d.protocol;
|
|
await sessionsListGuard.acquire(AppiumDriver.name, () => {
|
|
this.sessions[innerSessionId] = d;
|
|
});
|
|
} finally {
|
|
await pendingDriversGuard.acquire(AppiumDriver.name, () => {
|
|
_.pull(this.pendingDrivers[InnerDriver.name], d);
|
|
});
|
|
}
|
|
|
|
// this is an async function but we don't await it because it handles
|
|
// an out-of-band promise which is fulfilled if the inner driver
|
|
// unexpectedly shuts down
|
|
this.attachUnexpectedShutdownHandler(d, innerSessionId);
|
|
|
|
|
|
log.info(`New ${InnerDriver.name} session created successfully, session ` +
|
|
`${innerSessionId} added to master session list`);
|
|
|
|
// set the New Command Timeout for the inner driver
|
|
d.startNewCommandTimeout();
|
|
} catch (error) {
|
|
return {
|
|
protocol,
|
|
error,
|
|
};
|
|
}
|
|
|
|
return {
|
|
protocol,
|
|
value: [innerSessionId, dCaps, protocol]
|
|
};
|
|
}
|
|
|
|
async attachUnexpectedShutdownHandler (driver, innerSessionId) {
|
|
// Remove the session on unexpected shutdown, so that we are in a position
|
|
// to open another session later on.
|
|
// TODO: this should be removed and replaced by a onShutdown callback.
|
|
try {
|
|
await driver.onUnexpectedShutdown; // this is a cancellable promise
|
|
// if we get here, we've had an unexpected shutdown, so error
|
|
throw new Error('Unexpected shutdown');
|
|
} catch (e) {
|
|
if (e instanceof B.CancellationError) {
|
|
// if we cancelled the unexpected shutdown promise, that means we
|
|
// no longer care about it, and can safely ignore it
|
|
return;
|
|
}
|
|
log.warn(`Closing session, cause was '${e.message}'`);
|
|
log.info(`Removing session ${innerSessionId} from our master session list`);
|
|
await sessionsListGuard.acquire(AppiumDriver.name, () => {
|
|
delete this.sessions[innerSessionId];
|
|
});
|
|
}
|
|
}
|
|
|
|
async curSessionDataForDriver (InnerDriver) {
|
|
const sessions = await sessionsListGuard.acquire(AppiumDriver.name, () => this.sessions);
|
|
const data = _.values(sessions)
|
|
.filter((s) => s.constructor.name === InnerDriver.name)
|
|
.map((s) => s.driverData);
|
|
for (let datum of data) {
|
|
if (!datum) {
|
|
throw new Error(`Problem getting session data for driver type ` +
|
|
`${InnerDriver.name}; does it implement 'get ` +
|
|
`driverData'?`);
|
|
}
|
|
}
|
|
return data;
|
|
}
|
|
|
|
async deleteSession (sessionId) {
|
|
let protocol;
|
|
try {
|
|
let otherSessionsData = null;
|
|
let dstSession = null;
|
|
await sessionsListGuard.acquire(AppiumDriver.name, () => {
|
|
if (!this.sessions[sessionId]) {
|
|
return;
|
|
}
|
|
const curConstructorName = this.sessions[sessionId].constructor.name;
|
|
otherSessionsData = _.toPairs(this.sessions)
|
|
.filter(([key, value]) => value.constructor.name === curConstructorName && key !== sessionId)
|
|
.map(([, value]) => value.driverData);
|
|
dstSession = this.sessions[sessionId];
|
|
protocol = dstSession.protocol;
|
|
log.info(`Removing session ${sessionId} from our master session list`);
|
|
// regardless of whether the deleteSession completes successfully or not
|
|
// make the session unavailable, because who knows what state it might
|
|
// be in otherwise
|
|
delete this.sessions[sessionId];
|
|
});
|
|
return {
|
|
protocol,
|
|
value: await dstSession.deleteSession(sessionId, otherSessionsData),
|
|
};
|
|
} catch (e) {
|
|
log.error(`Had trouble ending session ${sessionId}: ${e.message}`);
|
|
return {
|
|
protocol,
|
|
error: e,
|
|
};
|
|
}
|
|
}
|
|
|
|
async executeCommand (cmd, ...args) {
|
|
// getStatus command should not be put into queue. If we do it as part of super.executeCommand, it will be added to queue.
|
|
// There will be lot of status commands in queue during createSession command, as createSession can take up to or more than a minute.
|
|
if (cmd === 'getStatus') {
|
|
return await this.getStatus();
|
|
}
|
|
|
|
if (isAppiumDriverCommand(cmd)) {
|
|
return await super.executeCommand(cmd, ...args);
|
|
}
|
|
|
|
const sessionId = _.last(args);
|
|
const dstSession = await sessionsListGuard.acquire(AppiumDriver.name, () => this.sessions[sessionId]);
|
|
if (!dstSession) {
|
|
throw new Error(`The session with id '${sessionId}' does not exist`);
|
|
}
|
|
|
|
let res = {
|
|
protocol: dstSession.protocol
|
|
};
|
|
|
|
try {
|
|
res.value = await dstSession.executeCommand(cmd, ...args);
|
|
} catch (e) {
|
|
res.error = e;
|
|
}
|
|
return res;
|
|
}
|
|
|
|
proxyActive (sessionId) {
|
|
const dstSession = this.sessions[sessionId];
|
|
return dstSession && _.isFunction(dstSession.proxyActive) && dstSession.proxyActive(sessionId);
|
|
}
|
|
|
|
getProxyAvoidList (sessionId) {
|
|
const dstSession = this.sessions[sessionId];
|
|
return dstSession ? dstSession.getProxyAvoidList() : [];
|
|
}
|
|
|
|
canProxy (sessionId) {
|
|
const dstSession = this.sessions[sessionId];
|
|
return dstSession && dstSession.canProxy(sessionId);
|
|
}
|
|
}
|
|
|
|
// help decide which commands should be proxied to sub-drivers and which
|
|
// should be handled by this, our umbrella driver
|
|
function isAppiumDriverCommand (cmd) {
|
|
return !isSessionCommand(cmd) || cmd === "deleteSession";
|
|
}
|
|
|
|
export { AppiumDriver };
|