UNPKG

appium

Version:

Automation for Apps.

370 lines (330 loc) 11.6 kB
import {fs} from '@appium/support'; import {ArgumentParser} from 'argparse'; import _ from 'lodash'; import path from 'path'; import { DRIVER_TYPE, EXT_SUBCOMMAND_DOCTOR, EXT_SUBCOMMAND_INSTALL, EXT_SUBCOMMAND_LIST, EXT_SUBCOMMAND_RUN, EXT_SUBCOMMAND_UNINSTALL, EXT_SUBCOMMAND_UPDATE, PLUGIN_TYPE, SERVER_SUBCOMMAND, SETUP_SUBCOMMAND } from '../constants'; import {finalizeSchema, getArgSpec, hasArgSpec} from '../schema'; import {rootDir} from '../config'; import {getExtensionArgs, getServerArgs} from './args'; import { DEFAULT_PLUGINS, SUBCOMMAND_MOBILE, SUBCOMMAND_DESKTOP, SUBCOMMAND_BROWSER, SUBCOMMAND_RESET, getPresetDrivers, determinePlatformName } from './setup-command'; export const EXTRA_ARGS = 'extraArgs'; /** * If the parsed args do not contain any of these values, then we * will automatically inject the `server` subcommand. */ const NON_SERVER_ARGS = Object.freeze( new Set([SETUP_SUBCOMMAND, DRIVER_TYPE, PLUGIN_TYPE, SERVER_SUBCOMMAND, '-h', '--help', '-v', '--version']) ); const version = fs.readPackageJsonFrom(rootDir).version; /** * A wrapper around `argparse` * * - Handles instantiation, configuration, and monkeypatching of an * `ArgumentParser` instance for Appium server and its extensions * - Handles error conditions, messages, and exit behavior */ class ArgParser { /** * @param {boolean} [debug] - If true, throw instead of exit on error. */ constructor(debug = false) { const prog = process.argv[1] ? path.basename(process.argv[1]) : 'appium'; const parser = new ArgumentParser({ add_help: true, description: 'A webdriver-compatible server that facilitates automation of web, mobile, and other ' + 'types of apps across various platforms.', prog, }); ArgParser._patchExit(parser); /** * Program name (typically `appium`) * @type {string} */ this.prog = prog; /** * If `true`, throw an error on parse failure instead of printing help * @type {boolean} */ this.debug = debug; /** * Wrapped `ArgumentParser` instance * @type {ArgumentParser} */ this.parser = parser; parser.add_argument('-v', '--version', { action: 'version', version, }); const subParsers = parser.add_subparsers({dest: 'subcommand'}); // add the 'server' subcommand, and store the raw arguments on the parser // object as a way for other parts of the code to work with the arguments // conceptually rather than just through argparse const serverArgs = ArgParser._addServerToParser(subParsers); this.rawArgs = serverArgs; // add the 'driver' and 'plugin' subcommands ArgParser._addExtensionCommandsToParser(subParsers); // add the 'setup' command ArgParser._addSetupToParser(subParsers); // backwards compatibility / drop-in wrapper /** * @type {ArgParser['parseArgs']} */ this.parse_args = this.parseArgs; } /** * Parse arguments from the command line. * * If no subcommand is passed in, this method will inject the `server` subcommand. * * `ArgParser.prototype.parse_args` is an alias of this method. * @template {import('appium/types').CliCommand} [Cmd=import('appium/types').CliCommandServer] * @param {string[]} [args] - Array of arguments, ostensibly from `process.argv`. Gathers args from `process.argv` if not provided. * @returns {import('appium/types').Args<Cmd>} - The parsed arguments */ parseArgs(args = process.argv.slice(2)) { if (!NON_SERVER_ARGS.has(args[0])) { args.unshift(SERVER_SUBCOMMAND); } try { const parsed = this.parser.parse_known_args(args); const [knownArgs, unknownArgs] = parsed; // XXX: you'd think that argparse, when given an alias for a subcommand, // would set this value to the original subcommand name, but it doesn't. if (knownArgs?.driverCommand === 'ls') { knownArgs.driverCommand = 'list'; } else if (knownArgs?.pluginCommand === 'ls') { knownArgs.pluginCommand = 'list'; } if ( unknownArgs?.length && (knownArgs.driverCommand === 'run' || knownArgs.pluginCommand === 'run') ) { return ArgParser._transformParsedArgs(knownArgs, unknownArgs); } else if (unknownArgs?.length) { throw new Error(`[ERROR] Unrecognized arguments: ${unknownArgs.join(' ')}`); } return ArgParser._transformParsedArgs(knownArgs); } catch (err) { if (this.debug) { throw err; } // this isn't tested via unit tests (we use `debug: true`) so may escape coverage. /* istanbul ignore next */ { // eslint-disable-next-line no-console console.error(); // need an extra space since argparse prints usage. // eslint-disable-next-line no-console console.error(err.message); process.exit(1); } } } /** * Given an object full of arguments as returned by `argparser.parse_args`, * expand the ones for extensions into a nested object structure and rename * keys to match the intended destination. * * E.g., `{'driver-foo-bar': baz}` becomes `{driver: {foo: {bar: 'baz'}}}` * @param {object} args * @param {string[]} [unknownArgs] * @returns {object} */ static _transformParsedArgs(args, unknownArgs = []) { const result = _.reduce( args, (unpacked, value, key) => { if (!_.isUndefined(value) && hasArgSpec(key)) { const {dest} = /** @type {import('../schema/arg-spec').ArgSpec} */ (getArgSpec(key)); _.set(unpacked, dest, value); } else { // this could be anything that _isn't_ a server arg unpacked[key] = value; } return unpacked; }, {} ); result[EXTRA_ARGS] = unknownArgs; return result; } /** * Patches the `exit()` method of the parser to throw an error, so we can handle it manually. * @param {ArgumentParser} parser */ static _patchExit(parser) { parser.exit = (code, msg) => { if (code) { throw new Error(msg); } process.exit(); }; } /** * * @param {import('argparse').SubParser} subParser * @returns {import('./args').ArgumentDefinitions} */ static _addServerToParser(subParser) { const serverParser = subParser.add_parser('server', { add_help: true, help: 'Start an Appium server', description: 'Start an Appium server (the "server" subcommand is optional)', }); ArgParser._patchExit(serverParser); const serverArgs = getServerArgs(); for (const [flagsOrNames, opts] of serverArgs) { // @ts-ignore TS doesn't like the spread operator here. serverParser.add_argument(...flagsOrNames, {...opts}); } return serverArgs; } /** * Adds extension sub-sub-commands to `driver`/`plugin` subcommands * @param {import('argparse').SubParser} subParsers */ static _addExtensionCommandsToParser(subParsers) { for (const type of /** @type {[DriverType, PluginType]} */ ([DRIVER_TYPE, PLUGIN_TYPE])) { const extParser = subParsers.add_parser(type, { add_help: true, help: `Manage Appium ${type}s`, description: `Manage Appium ${type}s using various subcommands`, }); ArgParser._patchExit(extParser); const extSubParsers = extParser.add_subparsers({ dest: `${type}Command`, }); const extensionArgs = getExtensionArgs(); /** * @type { {command: import('appium/types').CliExtensionSubcommand, args: import('./args').ArgumentDefinitions, help: string, aliases?: import('argparse').SubArgumentParserOptions['aliases']}[] } */ const parserSpecs = [ { command: EXT_SUBCOMMAND_LIST, args: extensionArgs[type].list, help: `List available and installed ${type}s`, aliases: ['ls'], }, { command: EXT_SUBCOMMAND_INSTALL, args: extensionArgs[type].install, help: `Install a ${type}`, }, { command: EXT_SUBCOMMAND_UNINSTALL, args: extensionArgs[type].uninstall, help: `Uninstall a ${type}`, }, { command: EXT_SUBCOMMAND_UPDATE, args: extensionArgs[type].update, help: `Update one or more installed ${type}s to the latest version`, }, { command: EXT_SUBCOMMAND_RUN, args: extensionArgs[type].run, help: `Run a script (if available) from the given ${type}`, }, { command: EXT_SUBCOMMAND_DOCTOR, args: extensionArgs[type].doctor, help: `Run doctor checks (if available) for the given ${type}`, }, ]; for (const {command, args, help, aliases} of parserSpecs) { const parser = extSubParsers.add_parser(command, {help, aliases: aliases ?? []}); ArgParser._patchExit(parser); for (const [flagsOrNames, opts] of args) { // add_argument mutates params so make sure to send in copies instead if (flagsOrNames.length === 2) { parser.add_argument(flagsOrNames[0], flagsOrNames[1], {...opts}); } else { parser.add_argument(flagsOrNames[0], {...opts}); } } } } } /** * Add subcommand and sub-sub commands for 'setup' subcommand. * @param {import('argparse').SubParser} subParser */ static _addSetupToParser(subParser) { const setupParser = subParser.add_parser('setup', { add_help: true, help: 'Batch install or uninstall Appium drivers and plugins', description: `Install a preset of official drivers/plugins compatible with the current host platform ` + `(${determinePlatformName()}). Existing drivers/plugins will remain. The default preset ` + `is "mobile". Providing the special "reset" subcommand will instead uninstall all ` + `drivers and plugins, and remove their related manifest files.`, }); ArgParser._patchExit(setupParser); const extSubParsers = setupParser.add_subparsers({ dest: `setupCommand`, }); const parserSpecs = [ { command: SUBCOMMAND_MOBILE, help: `The preset for mobile devices ` + `(drivers: ${_.join(getPresetDrivers(SUBCOMMAND_MOBILE), ',')}; plugins: ${DEFAULT_PLUGINS})` }, { command: SUBCOMMAND_BROWSER, help: `The preset for desktop browsers ` + `(drivers: ${_.join(getPresetDrivers(SUBCOMMAND_BROWSER), ',')}; plugins: ${DEFAULT_PLUGINS})` }, { command: SUBCOMMAND_DESKTOP, help: `The preset for desktop applications ` + `(drivers: ${_.join(getPresetDrivers(SUBCOMMAND_DESKTOP), ',')}; plugins: ${DEFAULT_PLUGINS})` }, { command: SUBCOMMAND_RESET, help: 'Remove all installed drivers and plugins' }, ]; for (const {command, help} of parserSpecs) { const parser = extSubParsers.add_parser(command, {help}); ArgParser._patchExit(parser); } } } /** * Creates a {@link ArgParser} instance; finalizes the config schema. * * @constructs ArgParser * @param {boolean} [debug] - If `true`, throw instead of exit upon parsing error * @returns {ArgParser} */ function getParser(debug) { finalizeSchema(); return new ArgParser(debug); } export {getParser, ArgParser}; /** * @typedef {import('@appium/types').DriverType} DriverType * @typedef {import('@appium/types').PluginType} PluginType */