UNPKG

appium-espresso-driver

Version:
413 lines 20.1 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.TEST_APK_PKG = exports.REQUIRED_PARAMS = exports.EspressoRunner = void 0; const driver_1 = require("appium/driver"); const asyncbox_1 = require("asyncbox"); const server_builder_1 = require("./server-builder"); const path_1 = __importDefault(require("path")); const support_1 = require("appium/support"); const bluebird_1 = __importDefault(require("bluebird")); const lodash_1 = __importDefault(require("lodash")); const utils_1 = require("./utils"); const axios_1 = __importDefault(require("axios")); const semver = __importStar(require("semver")); const TEST_SERVER_ROOT = path_1.default.resolve(__dirname, '..', '..', 'espresso-server'); const TEST_APK_PKG = 'io.appium.espressoserver.test'; exports.TEST_APK_PKG = TEST_APK_PKG; const REQUIRED_PARAMS = [ 'adb', 'tmpDir', 'host', 'systemPort', 'devicePort', 'appPackage', 'forceEspressoRebuild', ]; exports.REQUIRED_PARAMS = REQUIRED_PARAMS; const ESPRESSO_SERVER_LAUNCH_TIMEOUT_MS = 45000; const TARGET_PACKAGE_CONTAINER = '/data/local/tmp/espresso.apppackage'; class EspressoProxy extends driver_1.JWProxy { async proxyCommand(url, method, body = null) { const { crashed, exited } = this.instrumentationState; if (exited) { throw new driver_1.errors.InvalidContextError(`'${method} ${url}' cannot be proxied to Espresso server because ` + `the instrumentation process has ${crashed ? 'crashed' : 'been unexpectedly terminated'}. ` + `Check the Appium server log and the logcat output for more details`); } return await super.proxyCommand(url, method, body); } } class EspressoRunner { /** * * @param {import('@appium/types').AppiumLogger} log * @param {import('@appium/types').StringRecord} opts */ constructor(log, opts = {}) { for (const req of REQUIRED_PARAMS) { if (!opts || !support_1.util.hasValue(opts[req])) { throw new Error(`Option '${req}' is required!`); } this[req] = opts[req]; } this.log = log; const proxyOpts = { log, server: this.host, port: this.systemPort, keepAlive: true, }; if (opts.reqBasePath) { proxyOpts.reqBasePath = opts.reqBasePath; } this.jwproxy = new EspressoProxy(proxyOpts); this.proxyReqRes = this.jwproxy.proxyReqRes.bind(this.jwproxy); this.proxyCommand = this.jwproxy.command.bind(this.jwproxy); this.jwproxy.instrumentationState = { exited: false, crashed: false, }; const { manifestPayload } = (0, utils_1.getPackageInfoSync)(); const modServerName = support_1.fs.sanitizeName(`${TEST_APK_PKG}_${manifestPayload.version}_${this.appPackage}_${this.adb.curDeviceId}.apk`, { replacement: '-' }); this.modServerPath = path_1.default.resolve(this.tmpDir, modServerName); this.showGradleLog = opts.showGradleLog; this.espressoBuildConfig = opts.espressoBuildConfig; this.serverLaunchTimeout = opts.serverLaunchTimeout || ESPRESSO_SERVER_LAUNCH_TIMEOUT_MS; this.androidInstallTimeout = opts.androidInstallTimeout; this.disableSuppressAccessibilityService = opts.disableSuppressAccessibilityService; // Espresso Server app needs to be signed with same keyStore as appPackage if (opts.useKeystore && opts.keystorePath && opts.keystorePassword && opts.keyAlias && opts.keyPassword) { this.signingConfig = (0, server_builder_1.buildServerSigningConfig)({ keystoreFile: opts.keystorePath, keystorePassword: opts.keystorePassword, keyAlias: opts.keyAlias, keyPassword: opts.keyPassword }); } else { this.signingConfig = null; } } async isAppPackageChanged() { if (!await this.adb.fileExists(TARGET_PACKAGE_CONTAINER)) { this.log.debug('The previous target application package is unknown'); return true; } const previousAppPackage = (await this.adb.shell(['cat', TARGET_PACKAGE_CONTAINER])).trim(); this.log.debug(`The previous target application package was '${previousAppPackage}'. ` + `The current package is '${this.appPackage}'`); return previousAppPackage !== this.appPackage; } /** * Installs Espresso server apk on to the device or emulator. * Each adb command uses default timeout by them. */ async installServer() { const appState = await this.adb.getApplicationInstallState(this.modServerPath, TEST_APK_PKG); const shouldUninstallApp = [ this.adb.APP_INSTALL_STATE.OLDER_VERSION_INSTALLED, this.adb.APP_INSTALL_STATE.NEWER_VERSION_INSTALLED ].includes(appState); const shouldInstallApp = shouldUninstallApp || [ this.adb.APP_INSTALL_STATE.NOT_INSTALLED ].includes(appState); if (shouldUninstallApp) { this.log.info(`Uninstalling Espresso Test Server apk from the target device (pkg: '${TEST_APK_PKG}')`); try { await this.adb.uninstallApk(TEST_APK_PKG); } catch (err) { this.log.warn(`Error uninstalling '${TEST_APK_PKG}': ${err.message}`); } } if (shouldInstallApp) { this.log.info(`Installing Espresso Test Server apk from the target device (path: '${this.modServerPath}')`); try { await this.adb.install(this.modServerPath, { replace: false, timeout: this.androidInstallTimeout }); this.log.info(`Installed Espresso Test Server apk '${this.modServerPath}' (pkg: '${TEST_APK_PKG}')`); } catch (err) { throw this.log.errorWithException(`Cannot install '${this.modServerPath}' because of '${err.message}'`); } } } async installTestApk() { let rebuild = this.forceEspressoRebuild; if (rebuild) { this.log.debug(`'forceEspressoRebuild' capability is enabled`); } else if (await this.isAppPackageChanged()) { this.log.info(`Forcing Espresso server rebuild because of changed application package`); rebuild = true; } if (rebuild && await support_1.fs.exists(this.modServerPath)) { this.log.debug(`Deleting the obsolete Espresso server package '${this.modServerPath}'`); await support_1.fs.unlink(this.modServerPath); } if (!(await support_1.fs.exists(this.modServerPath))) { await this.buildNewModServer(); } const isSigned = await this.adb.checkApkCert(this.modServerPath, TEST_APK_PKG); if (!isSigned) { await this.adb.sign(this.modServerPath); } if ((rebuild || !isSigned) && await this.adb.uninstallApk(TEST_APK_PKG)) { this.log.info('Uninstalled the obsolete Espresso server package from the device under test'); } await this.installServer(); } async buildNewModServer() { let buildConfiguration = {}; if (this.espressoBuildConfig) { let buildConfigurationStr; if (await support_1.fs.exists(this.espressoBuildConfig)) { this.log.info(`Loading the build configuration from '${this.espressoBuildConfig}'`); buildConfigurationStr = await support_1.fs.readFile(this.espressoBuildConfig, 'utf8'); } else { this.log.info(`Loading the build configuration from 'espressoBuildConfig' capability`); buildConfigurationStr = this.espressoBuildConfig; } try { buildConfiguration = JSON.parse(buildConfigurationStr); } catch (e) { this.log.error('Cannot parse the build configuration JSON', e); throw e; } } const dirName = support_1.fs.sanitizeName(`espresso-server-${this.adb.curDeviceId}`, { replacement: '-', }); const serverPath = path_1.default.resolve(this.tmpDir, dirName); this.log.info(`Building espresso server in '${serverPath}'`); this.log.debug(`The build folder root could be customized by changing the 'tmpDir' capability`); await support_1.fs.rimraf(serverPath); await (0, support_1.mkdirp)(serverPath); this.log.debug(`Copying espresso server template from ('${TEST_SERVER_ROOT}' to '${serverPath}')`); await (0, utils_1.copyGradleProjectRecursively)(TEST_SERVER_ROOT, serverPath); this.log.debug('Bulding espresso server'); await new server_builder_1.ServerBuilder(this.log, { serverPath, buildConfiguration, showGradleLog: this.showGradleLog, testAppPackage: this.appPackage, signingConfig: this.signingConfig }).build(); const apkPath = path_1.default.resolve(serverPath, 'app', 'build', 'outputs', 'apk', 'androidTest', 'debug', 'app-debug-androidTest.apk'); this.log.debug(`Copying built apk from '${apkPath}' to '${this.modServerPath}'`); await support_1.fs.copyFile(apkPath, this.modServerPath); } async cleanupSessionLeftovers() { this.log.debug('Performing cleanup of automation leftovers'); try { const { value } = (await (0, axios_1.default)({ url: `http://${this.host}:${this.systemPort}/sessions`, timeout: 500, })).data; const activeSessionIds = value.map((sess) => sess.id); if (activeSessionIds.length) { this.log.debug(`The following obsolete sessions are still running: ${JSON.stringify(activeSessionIds)}`); this.log.debug('Cleaning up the obsolete sessions'); await bluebird_1.default.all(activeSessionIds.map((id) => (0, axios_1.default)({ url: `http://${this.host}:${this.systemPort}/session/${id}`, method: 'DELETE', }))); // Let all sessions to be properly terminated before continuing await bluebird_1.default.delay(1000); } else { this.log.debug('No obsolete sessions have been detected'); } } catch (e) { this.log.debug(`No obsolete sessions have been detected (${e.message})`); } } /** * @param {string} driverVersion * @param {import('@appium/types').StringRecord} serverStatus * @returns {Promise<boolean>} */ async _verifyServerStatus(driverVersion, serverStatus) { if (!lodash_1.default.isPlainObject(serverStatus) || !lodash_1.default.isPlainObject(serverStatus.build)) { throw this.log.errorWithException(`The Espresso server version integrated with the application under test is not compatible ` + `with the current driver version '${driverVersion}'.`); } const { build: { version: serverVersion, packageName: serverPackageName, } } = serverStatus; const appLabel = serverPackageName ? `'${serverPackageName}' application` : 'application under test'; const parsedServerVersion = semver.coerce(serverVersion); const parsedDriverVersion = semver.coerce(driverVersion); if (parsedServerVersion && parsedDriverVersion) { if (parsedServerVersion.major !== parsedDriverVersion.major) { throw this.log.errorWithException(`The Espresso server version '${serverVersion}' integrated with the ${appLabel} is not compatible ` + `with the current driver version '${driverVersion}'.`); } else if (parsedServerVersion.minor < parsedDriverVersion.minor) { this.log.warn(`The Espresso server version integrated with the ${appLabel} might not be compatible ` + `with the current driver version (${serverVersion} < ${driverVersion})'.`); } } else { const warnMessage = parsedServerVersion ? `The Espresso driver version '${driverVersion}' ` + `cannot be parsed. It might be incompatible with the current server '${serverVersion}' ` + `integrated with the ${appLabel}.` : `The Espresso server version '${serverVersion}' integrated with the ${appLabel} ` + `cannot be parsed. It might be incompatible with the current driver ` + `version '${driverVersion}'.`; this.log.warn(warnMessage); } if (this.appPackage && serverPackageName && this.appPackage !== serverPackageName) { throw this.log.errorWithException(`The Espresso server that is listening on the device under tests is built for a different ` + `application package (${serverPackageName} !== ${this.appPackage}).`); } return true; } async startSession(caps) { await this.cleanupSessionLeftovers(); /** @type {string[]} */ const cmd = [ 'shell', 'am', 'instrument', '-w', '-e', 'debug', String(process.env.ESPRESSO_JAVA_DEBUG === 'true'), '-e', 'disableAnalytics', 'true', // To avoid unexpected error by google analytics ]; if (lodash_1.default.isBoolean(this.disableSuppressAccessibilityService)) { cmd.push('-e', 'DISABLE_SUPPRESS_ACCESSIBILITY_SERVICES', String(this.disableSuppressAccessibilityService)); } cmd.push(`${TEST_APK_PKG}/androidx.test.runner.AndroidJUnitRunner`); const { manifestPayload } = await (0, utils_1.getPackageInfo)(); this.log.info(`Starting Espresso Server v${manifestPayload.version} with cmd: adb ${cmd.join(' ')}`); let hasSocketError = false; // start the instrumentation process this.jwproxy.instrumentationState = { exited: false, crashed: false, }; this.instProcess = this.adb.createSubProcess(cmd); this.instProcess.on('exit', (code, signal) => { this.log.info(`Instrumentation process exited with code ${code} from signal ${signal}`); this.jwproxy.instrumentationState.exited = true; }); this.instProcess.on('output', (stdout, stderr) => { const line = stdout || stderr; if (lodash_1.default.isEmpty(line.trim())) { // Do not print empty lines into the system log return; } this.log.debug(`[Instrumentation] ${line.trim()}`); // A 'SocketException' indicates that we couldn't connect to the Espresso server, // because the INTERNET permission is not set if (line.toLowerCase().includes('java.net.socketexception')) { hasSocketError = true; } else if (line.includes('Process crashed')) { this.jwproxy.instrumentationState.crashed = true; } }); const timer = new support_1.timing.Timer().start(); await this.instProcess.start(0); this.log.info(`Waiting up to ${this.serverLaunchTimeout}ms for Espresso server to be online`); try { await (0, asyncbox_1.waitForCondition)(async () => { if (hasSocketError) { throw this.log.errorWithException(`Espresso server has failed to start due to an unexpected exception. ` + `Make sure the 'INTERNET' permission is requested in the Android manifest of your ` + `application under test (<uses-permission android:name="android.permission.INTERNET" />)`); } else if (this.jwproxy.instrumentationState.exited) { throw this.log.errorWithException(`Espresso server process has been unexpectedly terminated. ` + `Check the Appium server log and the logcat output for more details`); } let serverStatus; try { (serverStatus = /** @type {import('@appium/types').StringRecord} */ (await this.jwproxy.command('/status', 'GET'))); } catch { return false; } return await this._verifyServerStatus(manifestPayload.version, serverStatus); }, { waitMs: this.serverLaunchTimeout, intervalMs: 500, }); } catch (e) { if (/Condition unmet/.test(e.message)) { throw this.log.errorWithException(`Timed out waiting for Espresso server to be ` + `online within ${this.serverLaunchTimeout}ms. The timeout value could be ` + `customized using 'espressoServerLaunchTimeout' capability`); } throw e; } this.log.info(`Espresso server is online. ` + `The initialization process took ${timer.getDuration().asMilliSeconds.toFixed(0)}ms`); this.log.info('Starting the session'); await this.jwproxy.command('/session', 'POST', { capabilities: { firstMatch: [caps], alwaysMatch: {} } }); await this.recordTargetAppPackage(); } async recordTargetAppPackage() { await this.adb.shell([`echo "${this.appPackage}" > "${TARGET_PACKAGE_CONTAINER}"`]); this.log.info(`Recorded the target application package '${this.appPackage}' to ${TARGET_PACKAGE_CONTAINER}`); } async deleteSession() { this.log.debug('Deleting Espresso server session'); // rely on jwproxy's intelligence to know what we're talking about and // delete the current session try { await this.jwproxy.command('/', 'DELETE'); } catch (err) { this.log.warn(`Did not get confirmation Espresso deleteSession worked; ` + `Error was: ${err}`); } if (this.instProcess && this.instProcess.isRunning) { await this.instProcess.stop(); } } } exports.EspressoRunner = EspressoRunner; exports.default = EspressoRunner; //# sourceMappingURL=espresso-runner.js.map