UNPKG

appium

Version:

Automation for Apps.

390 lines (353 loc) 11.7 kB
/* eslint-disable no-console */ import _ from 'lodash'; import {system, fs, npm} from '@appium/support'; import axios from 'axios'; import {exec} from 'teen_process'; import * as semver from 'semver'; import os from 'node:os'; import {npmPackage} from './utils'; import B from 'bluebird'; import {getDefaultsForSchema, getAllArgSpecs} from './schema/schema'; export const APPIUM_VER = npmPackage.version; const ENGINES = /** @type {Record<string,string>} */ (npmPackage.engines); const MIN_NODE_VERSION = ENGINES.node; export const rootDir = fs.findRoot(__dirname); const GIT_BINARY = `git${system.isWindows() ? '.exe' : ''}`; const GITHUB_API = 'https://api.github.com/repos/appium/appium'; /** * @type {import('appium/types').BuildInfo} */ const BUILD_INFO = { version: APPIUM_VER, }; function getNodeVersion() { return /** @type {import('semver').SemVer} */ (semver.coerce(process.version)); } /** * @param {boolean} [useGithubApiFallback] * @returns {Promise<void>} */ export async function updateBuildInfo(useGithubApiFallback = false) { const sha = await getGitRev(useGithubApiFallback); if (!sha) { return; } BUILD_INFO['git-sha'] = sha; const buildTimestamp = await getGitTimestamp(sha, useGithubApiFallback); if (buildTimestamp) { BUILD_INFO.built = buildTimestamp; } } /** @type {() => Promise<string?>} */ const getFullGitPath = _.memoize(async function getFullGitPath() { try { return await fs.which(GIT_BINARY); } catch { return null; } }); /** * Prints server debug into into the console. * * @param {{ * driverConfig: import('./extension/driver-config').DriverConfig, * pluginConfig: import('./extension/plugin-config').PluginConfig, * appiumHome: string * }} info * @returns {Promise<void>} */ export async function showDebugInfo({driverConfig, pluginConfig, appiumHome}) { const getNpmVersion = async () => { const {stdout} = await npm.exec('--version', [], {cwd: process.cwd()}); return _.trim(stdout); }; const findNpmLocation = async () => await fs.which(system.isWindows() ? 'npm.cmd' : 'npm'); const [npmVersion, npmLocation] = await B.all([ ...([getNpmVersion, findNpmLocation].map((f) => getSafeResult(f, 'unknown'))), /** @type {any} */ (updateBuildInfo()), ]); const debugInfo = { os: { platform: os.platform(), release: os.release(), arch: os.arch(), homedir: os.homedir(), username: os.userInfo().username, }, node: { version: process.version, arch: process.arch, cwd: process.cwd(), argv: process.argv, env: process.env, npm: { location: npmLocation, version: npmVersion, }, }, appium: { location: rootDir, homedir: appiumHome, build: getBuildInfo(), drivers: driverConfig.installedExtensions, plugins: pluginConfig.installedExtensions, }, }; console.log(JSON.stringify(debugInfo, null, 2)); } /** * @param {boolean} [useGithubApiFallback] * @returns {Promise<string?>} */ export async function getGitRev(useGithubApiFallback = false) { const fullGitPath = await getFullGitPath(); if (fullGitPath) { try { const {stdout} = await exec(fullGitPath, ['rev-parse', 'HEAD'], { cwd: __dirname, }); return stdout.trim(); } catch {} } if (!useGithubApiFallback) { return null; } // If the package folder is not a valid git repository // then fetch the corresponding tag info from GitHub try { return ( await axios.get(`${GITHUB_API}/git/refs/tags/appium@${APPIUM_VER}`, { headers: { 'User-Agent': `Appium ${APPIUM_VER}`, }, }) ).data?.object?.sha; } catch {} return null; } /** * @param {string} commitSha * @param {boolean} [useGithubApiFallback] * @returns {Promise<string?>} */ async function getGitTimestamp(commitSha, useGithubApiFallback = false) { const fullGitPath = await getFullGitPath(); if (fullGitPath) { try { const {stdout} = await exec(fullGitPath, ['show', '-s', '--format=%ci', commitSha], { cwd: __dirname, }); return stdout.trim(); } catch {} } if (!useGithubApiFallback) { return null; } try { return ( await axios.get(`${GITHUB_API}/git/tags/${commitSha}`, { headers: { 'User-Agent': `Appium ${APPIUM_VER}`, }, }) ).data?.tagger?.date; } catch {} return null; } /** * Mutable object containing Appium build information. By default it * only contains the Appium version, but is updated with the build timestamp * and git commit hash asynchronously as soon as `updateBuildInfo` is called * and succeeds. * @returns {import('appium/types').BuildInfo} */ export function getBuildInfo() { return BUILD_INFO; } /** * @returns {void} * @throws {Error} If Node version is outside of the supported range */ export function checkNodeOk() { const version = getNodeVersion(); if (!semver.satisfies(version, MIN_NODE_VERSION)) { throw new Error( `Node version must be at least ${MIN_NODE_VERSION}; current is ${version.version}` ); } } export async function showBuildInfo() { await updateBuildInfo(true); console.log(JSON.stringify(getBuildInfo())); } /** * Returns k/v pairs of server arguments which are _not_ the defaults. * @param {Args} parsedArgs * @returns {Args} */ export function getNonDefaultServerArgs(parsedArgs) { /** * Flattens parsed args into a single level object for comparison with * flattened defaults across server args and extension args. * @param {Args} args * @returns {Record<string, { value: any, argSpec: ArgSpec }>} */ const flatten = (args) => { const argSpecs = getAllArgSpecs(); const flattened = _.reduce( [...argSpecs.values()], (acc, argSpec) => { if (_.has(args, argSpec.dest)) { acc[argSpec.dest] = {value: _.get(args, argSpec.dest), argSpec}; } return acc; }, /** @type {Record<string, { value: any, argSpec: ArgSpec }>} */ ({}) ); return flattened; }; const args = flatten(parsedArgs); // hopefully these function names are descriptive enough const typesDiffer = /** @param {string} dest */ (dest) => typeof args[dest].value !== typeof defaultsFromSchema[dest]; const defaultValueIsArray = /** @param {string} dest */ (dest) => _.isArray(defaultsFromSchema[dest]); const argsValueIsArray = /** @param {string} dest */ (dest) => _.isArray(args[dest].value); const arraysDiffer = /** @param {string} dest */ (dest) => _.gt(_.size(_.difference(args[dest].value, defaultsFromSchema[dest])), 0); const valuesDiffer = /** @param {string} dest */ (dest) => args[dest].value !== defaultsFromSchema[dest]; const defaultIsDefined = /** @param {string} dest */ (dest) => !_.isUndefined(defaultsFromSchema[dest]); // note that `_.overEvery` is like an "AND", and `_.overSome` is like an "OR" const argValueNotArrayOrArraysDiffer = _.overSome([_.negate(argsValueIsArray), arraysDiffer]); const defaultValueNotArrayAndValuesDiffer = _.overEvery([ _.negate(defaultValueIsArray), valuesDiffer, ]); /** * This used to be a hideous conditional, but it's broken up into a hideous function instead. * hopefully this makes things a little more understandable. * - checks if the default value is defined * - if so, and the default is not an array: * - ensures the types are the same * - ensures the values are equal * - if so, and the default is an array: * - ensures the args value is an array * - ensures the args values do not differ from the default values * @type {(dest: string) => boolean} */ const isNotDefault = _.overEvery([ defaultIsDefined, _.overSome([ typesDiffer, _.overEvery([defaultValueIsArray, argValueNotArrayOrArraysDiffer]), defaultValueNotArrayAndValuesDiffer, ]), ]); const defaultsFromSchema = getDefaultsForSchema(true); return _.reduce( _.pickBy(args, (__, key) => isNotDefault(key)), // explodes the flattened object back into nested one (acc, {value, argSpec}) => _.set(acc, argSpec.dest, value), /** @type {Args} */ ({}) ); } /** * Compacts an object for {@link showConfig}: * 1. Removes `subcommand` key/value * 2. Removes `undefined` values * 3. Removes empty objects (but not `false` values) * Does not operate recursively. */ const compactConfig = _.partial( _.omitBy, _, (value, key) => key === 'subcommand' || _.isUndefined(value) || (_.isObject(value) && _.isEmpty(value)) ); /** * Shows a breakdown of the current config after CLI params, config file loaded & defaults applied. * * The actual shape of `preConfigParsedArgs` and `defaults` does not matter for the purposes of this function, * but it's intended to be called with values of type {@link ParsedArgs} and `DefaultValues<true>`, respectively. * * @param {Partial<ParsedArgs>} nonDefaultPreConfigParsedArgs - Parsed CLI args (or param to `init()`) before config & defaults applied * @param {import('./config-file').ReadConfigFileResult} configResult - Result of attempting to load a config file. _Must_ be normalized * @param {Partial<ParsedArgs>} defaults - Configuration defaults from schemas * @param {ParsedArgs} parsedArgs - Entire parsed args object */ export function showConfig(nonDefaultPreConfigParsedArgs, configResult, defaults, parsedArgs) { console.log('Appium Configuration\n'); console.log('from defaults:\n'); console.dir(compactConfig(defaults)); if (configResult.config) { console.log(`\nfrom config file at ${configResult.filepath}:\n`); console.dir(compactConfig(configResult.config)); } else { console.log(`\n(no configuration file loaded)`); } const compactedNonDefaultPreConfigArgs = compactConfig(nonDefaultPreConfigParsedArgs); if (_.isEmpty(compactedNonDefaultPreConfigArgs)) { console.log(`\n(no CLI parameters provided)`); } else { console.log('\nvia CLI or function call:\n'); console.dir(compactedNonDefaultPreConfigArgs); } console.log('\nfinal configuration:\n'); console.dir(compactConfig(parsedArgs)); } /** * @param {string} root * @param {boolean} [requireWriteable=true] * @param {string} [displayName='folder path'] * @throws {Error} */ export async function requireDir(root, requireWriteable = true, displayName = 'folder path') { let stat; try { stat = await fs.stat(root); } catch (e) { if (e.code === 'ENOENT') { try { await fs.mkdir(root, {recursive: true}); return; } catch {} } throw new Error(`The ${displayName} '${root}' must exist and be a valid directory`); } if (stat && !stat.isDirectory()) { throw new Error(`The ${displayName} '${root}' must be a valid directory`); } if (requireWriteable) { try { await fs.access(root, fs.constants.W_OK); } catch { throw new Error( `The ${displayName} '${root}' must be ` + `writeable for the current user account '${os.userInfo().username}'` ); } } } /** * Calculates the result of the given function and return its value * or the default one if there was an exception. * * @template T * @param {() => Promise<T>} f * @param {T} defaultValue * @returns {Promise<T>} */ async function getSafeResult(f, defaultValue) { try { return await f(); } catch { return defaultValue; } } /** * @typedef {import('appium/types').ParsedArgs} ParsedArgs * @typedef {import('appium/types').Args} Args * @typedef {import('./schema/arg-spec').ArgSpec} ArgSpec */