mirror of
https://github.com/appium/appium.git
synced 2026-10-27 03:25:54 +08:00
feat(appium): add --show-config (#16207)
* feat(appium): rename --show-config to --show-build-info * feat(appium): add --show-config This adds a new CLI flag: `--show-config`. When called, this will dump the values of a) arguments to the `init()` function of `main.js` _or_ args passed thru CLI, b) any config pulled from a config file, and c) the defaults as defined in schemas. Each section is separated, and `undefined` values are omitted. Instead of a JSON blob, the output just uses `console.dir()`. To make this work, some refactors were needed: - Use of `store_true` "action" for `argparse` in are now `store_const` with `const: true` because `store_true` sets the value to `false` if the flag is not present. This essentially assigns the default value of the flag (`false`) before it should; default values must come from the schemas. - Along those lines, removed default values from the "extra" boolean args in `args.js` - The CLI parser now ignores `undefined` values of arguments Other: - Created some types for extension classes and added the `server` prop to `AppiumDriver`. - Enabled TS checks on `lib/config.js` (where `showConfig()` lives) and `test/config-specs.js` - Removed unnecessary call to `validate()` in `preflightChecks()`; CLI arguments are validated individually (defined in `lib/schema/cli-args.js`) and the config file is validated separately (defined in `lib/config-file.js`). - Add more types to `main.js`; rewrite `getServerUpdaters` in lodashese for fun (which I am happy to revert) - Fix missing type aliases in `test/cli/cli-helpers.js` - Misc types/refactors in `config-specs.js` - Update parser tests to reflect that `undefined` values and empty objects are eliminated from the parsed arguments Notes: - The `showBuildInfo()` test in `config-specs.js` (which is ostensibly a unit test) forks `git` and also seems to require network connectivity, at least on my box. This stuff should be stubbed out or moved into an E2E test. Resolves #15672 * chore(appium): remove unused options in `toParserArgs()`
This commit is contained in:
@@ -53,6 +53,7 @@ class AppiumDriver extends BaseDriver {
|
||||
// It is not recommended to access this property directly from the outside
|
||||
this.pendingDrivers = {};
|
||||
|
||||
/** @type {PluginExtensionClass[]} */
|
||||
this.pluginClasses = []; // list of which plugins are active
|
||||
this.sessionPlugins = {}; // map of sessions to actual plugin instances per session
|
||||
this.sessionlessPlugins = []; // some commands are sessionless, so we need a set of plugins for them
|
||||
@@ -61,9 +62,12 @@ class AppiumDriver extends BaseDriver {
|
||||
updateBuildInfo();
|
||||
}
|
||||
|
||||
/** @type {DriverConfig|undefined} */
|
||||
/** @type {import('./driver-config').default|undefined} */
|
||||
driverConfig;
|
||||
|
||||
/** @type {import('express').Express|undefined} */
|
||||
server;
|
||||
|
||||
/**
|
||||
* Cancel commands queueing for the umbrella Appium driver
|
||||
*/
|
||||
@@ -670,3 +674,23 @@ export class NoDriverProxyCommandError extends Error {
|
||||
}
|
||||
|
||||
export { AppiumDriver };
|
||||
|
||||
|
||||
/**
|
||||
* @typedef {Object} StaticExtMembers
|
||||
* @property {(app: import('express').Express, httpServer: import('http').Server) => import('type-fest').Promisable<void>} [updateServer]
|
||||
* @property {import('@appium/base-driver').MethodMap} [newMethodMap]
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} StaticPluginMembers
|
||||
* @property {string} pluginName
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {import('type-fest').Class<unknown> & StaticPluginMembers & StaticExtMembers} PluginExtensionClass
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {import('type-fest').Class<unknown> & StaticExtMembers} DriverExtensionClass
|
||||
*/
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// @ts-check
|
||||
|
||||
// @ts-ignore
|
||||
import { DEFAULT_BASE_PATH } from '@appium/base-driver';
|
||||
import _ from 'lodash';
|
||||
import DriverConfig from '../driver-config';
|
||||
import { APPIUM_HOME, DRIVER_TYPE, INSTALL_TYPES, PLUGIN_TYPE } from '../extension-config';
|
||||
@@ -175,13 +174,7 @@ function makeRunArgs (type) {
|
||||
*/
|
||||
function getServerArgs () {
|
||||
return new Map([
|
||||
...toParserArgs({
|
||||
overrides: {
|
||||
basePath: {
|
||||
default: DEFAULT_BASE_PATH
|
||||
},
|
||||
}
|
||||
}),
|
||||
...toParserArgs(),
|
||||
...serverArgsDisallowedInConfig,
|
||||
]);
|
||||
}
|
||||
@@ -195,21 +188,31 @@ const serverArgsDisallowedInConfig = new Map([
|
||||
['--shell'],
|
||||
{
|
||||
required: false,
|
||||
default: null,
|
||||
help: 'Enter REPL mode',
|
||||
action: 'store_true',
|
||||
action: 'store_const',
|
||||
const: true,
|
||||
dest: 'shell',
|
||||
},
|
||||
],
|
||||
[
|
||||
['--show-build-info'],
|
||||
{
|
||||
dest: 'showBuildInfo',
|
||||
action: 'store_const',
|
||||
const: true,
|
||||
required: false,
|
||||
help: 'Show info about the Appium build and exit',
|
||||
},
|
||||
],
|
||||
[
|
||||
['--show-config'],
|
||||
{
|
||||
default: false,
|
||||
dest: 'showConfig',
|
||||
action: 'store_true',
|
||||
action: 'store_const',
|
||||
const: true,
|
||||
required: false,
|
||||
help: 'Show info about the appium server configuration and exit',
|
||||
},
|
||||
help: 'Show the current Appium configuration and exit',
|
||||
}
|
||||
],
|
||||
[
|
||||
['--config'],
|
||||
|
||||
@@ -146,7 +146,7 @@ class ArgParser {
|
||||
return _.reduce(
|
||||
args,
|
||||
(unpacked, value, key) => {
|
||||
if (hasArgSpec(key)) {
|
||||
if (!_.isUndefined(value) && hasArgSpec(key)) {
|
||||
const {dest} = /** @type {import('../schema/arg-spec').ArgSpec} */(getArgSpec(key));
|
||||
_.set(unpacked, dest, value);
|
||||
} else {
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// @ts-check
|
||||
|
||||
/* eslint-disable no-console */
|
||||
import _ from 'lodash';
|
||||
import { mkdirp, system, fs } from '@appium/support';
|
||||
import axios from 'axios';
|
||||
@@ -22,7 +25,7 @@ const BUILD_INFO = {
|
||||
};
|
||||
|
||||
function getNodeVersion () {
|
||||
return semver.coerce(process.version);
|
||||
return /** @type {import('semver').SemVer} */(semver.coerce(process.version));
|
||||
}
|
||||
|
||||
async function updateBuildInfo (useGithubApiFallback = false) {
|
||||
@@ -42,7 +45,7 @@ async function updateBuildInfo (useGithubApiFallback = false) {
|
||||
*
|
||||
* This is needed because Appium cannot assume `package.json` and `.git` are in the same
|
||||
* directory. Monorepos, see?
|
||||
* @returns {string|void} Path to dir or `undefined` if not found
|
||||
* @returns {Promise<string|undefined>} Path to dir or `undefined` if not found
|
||||
*/
|
||||
async function findGitRoot () {
|
||||
return await findUp(GIT_META_ROOT, {cwd: rootDir, type: 'directory'});
|
||||
@@ -80,6 +83,11 @@ async function getGitRev (useGithubApiFallback = false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} commitSha
|
||||
* @param {boolean} [useGithubApiFallback]
|
||||
* @returns {Promise<number?>}
|
||||
*/
|
||||
async function getGitTimestamp (commitSha, useGithubApiFallback = false) {
|
||||
const gitRoot = await findGitRoot();
|
||||
if (gitRoot) {
|
||||
@@ -144,30 +152,30 @@ function warnNodeDeprecations () {
|
||||
// }
|
||||
}
|
||||
|
||||
async function showConfig () {
|
||||
async function showBuildInfo () {
|
||||
await updateBuildInfo(true);
|
||||
console.log(JSON.stringify(getBuildInfo())); // eslint-disable-line no-console
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns k/v pairs of server arguments which are _not_ the defaults.
|
||||
* @param {import('../types/types').ParsedArgs} args
|
||||
* @returns {Partial<import('../types/types').ParsedArgs>}
|
||||
* @param {ParsedArgs} args
|
||||
* @returns {Partial<ParsedArgs>}
|
||||
*/
|
||||
function getNonDefaultServerArgs (args) {
|
||||
// hopefully these function names are descriptive enough
|
||||
|
||||
const typesDiffer = (dest) => typeof args[dest] !== typeof defaultsFromSchema[dest];
|
||||
const typesDiffer = /** @param {string} dest */(dest) => typeof args[dest] !== typeof defaultsFromSchema[dest];
|
||||
|
||||
const defaultValueIsArray = (dest) => _.isArray(defaultsFromSchema[dest]);
|
||||
const defaultValueIsArray = /** @param {string} dest */(dest) => _.isArray(defaultsFromSchema[dest]);
|
||||
|
||||
const argsValueIsArray = (dest) => _.isArray(args[dest]);
|
||||
const argsValueIsArray = /** @param {string} dest */(dest) => _.isArray(args[dest]);
|
||||
|
||||
const arraysDiffer = (dest) => _.size(_.difference(args[dest], defaultsFromSchema[dest])) > 0;
|
||||
const arraysDiffer = /** @param {string} dest */(dest) => _.gt(_.size(_.difference(args[dest], defaultsFromSchema[dest])), 0);
|
||||
|
||||
const valuesDiffer = (dest) => args[dest] !== defaultsFromSchema[dest];
|
||||
const valuesDiffer = /** @param {string} dest */(dest) => args[dest] !== defaultsFromSchema[dest];
|
||||
|
||||
const defaultIsDefined = (dest) => !_.isUndefined(defaultsFromSchema[dest]);
|
||||
const defaultIsDefined = /** @param {string} dest */(dest) => !_.isUndefined(defaultsFromSchema[dest]);
|
||||
|
||||
// note that `_.overEvery` is like an "AND", and `_.overSome` is like an "OR"
|
||||
|
||||
@@ -209,6 +217,46 @@ function getNonDefaultServerArgs (args) {
|
||||
return _.pickBy(args, (__, key) => isNotDefault(key));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {object} preConfigParsedArgs - 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
|
||||
* @param {object} defaults - Configuration defaults from schemas
|
||||
*/
|
||||
function showConfig (preConfigParsedArgs, configResult, defaults) {
|
||||
console.log('Appium Configuration\n');
|
||||
if (configResult.config) {
|
||||
console.log(`via config file at ${configResult.filepath}:\n`);
|
||||
console.dir(compactConfig(configResult.config));
|
||||
} else {
|
||||
console.log(`(no configuration file loaded)\n`);
|
||||
}
|
||||
console.log('via CLI or function call:\n');
|
||||
console.dir(compactConfig(preConfigParsedArgs));
|
||||
console.log('\nvia defaults:\n');
|
||||
console.dir(compactConfig(defaults));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} tmpDir
|
||||
*/
|
||||
async function validateTmpDir (tmpDir) {
|
||||
try {
|
||||
await mkdirp(tmpDir);
|
||||
@@ -219,7 +267,11 @@ async function validateTmpDir (tmpDir) {
|
||||
}
|
||||
|
||||
export {
|
||||
getBuildInfo, checkNodeOk, showConfig,
|
||||
getBuildInfo, checkNodeOk, showBuildInfo,
|
||||
warnNodeDeprecations, validateTmpDir, getNonDefaultServerArgs,
|
||||
getGitRev, APPIUM_VER, updateBuildInfo
|
||||
getGitRev, APPIUM_VER, updateBuildInfo, showConfig
|
||||
};
|
||||
|
||||
/**
|
||||
* @typedef {import('../types/types').ParsedArgs} ParsedArgs
|
||||
*/
|
||||
|
||||
@@ -270,7 +270,7 @@ export default class ExtensionConfig {
|
||||
/**
|
||||
* Loads extension and returns its main class
|
||||
* @param {string} extName
|
||||
* @returns {(...args: any[]) => object }
|
||||
* @returns {import('type-fest').Class<unknown>}
|
||||
*/
|
||||
require (extName) {
|
||||
const {mainClass} = this.installedExtensions[extName];
|
||||
|
||||
+58
-20
@@ -3,6 +3,7 @@
|
||||
// transpile:main
|
||||
// @ts-check
|
||||
|
||||
import logger from './logger'; // logger needs to remain first of imports
|
||||
// @ts-ignore
|
||||
import { routeConfiguringFunction as makeRouter, server as baseServer } from '@appium/base-driver';
|
||||
import { logger as logFactory, util } from '@appium/support';
|
||||
@@ -12,13 +13,12 @@ import { AppiumDriver } from './appium';
|
||||
import { driverConfig, pluginConfig, USE_ALL_PLUGINS } from './cli/args';
|
||||
import { runExtensionCommand } from './cli/extension';
|
||||
import { default as getParser, SERVER_SUBCOMMAND } from './cli/parser';
|
||||
import { APPIUM_VER, checkNodeOk, getGitRev, getNonDefaultServerArgs, showConfig, validateTmpDir, warnNodeDeprecations } from './config';
|
||||
import { APPIUM_VER, checkNodeOk, getGitRev, getNonDefaultServerArgs, showBuildInfo, validateTmpDir, warnNodeDeprecations, showConfig } from './config';
|
||||
import { readConfigFile } from './config-file';
|
||||
import { DRIVER_TYPE, PLUGIN_TYPE } from './extension-config';
|
||||
import registerNode from './grid-register';
|
||||
import logger from './logger'; // logger needs to remain first of imports
|
||||
import { init as logsinkInit } from './logsink';
|
||||
import { getDefaultsForSchema, validate } from './schema/schema';
|
||||
import { getDefaultsForSchema } from './schema/schema';
|
||||
import { inspect } from './utils';
|
||||
|
||||
/**
|
||||
@@ -32,14 +32,12 @@ async function preflightChecks (args, throwInsteadOfExit = false) {
|
||||
if (args.longStacktrace) {
|
||||
require('longjohn').async_trace_limit = -1;
|
||||
}
|
||||
if (args.showConfig) {
|
||||
await showConfig();
|
||||
if (args.showBuildInfo) {
|
||||
await showBuildInfo();
|
||||
process.exit(0);
|
||||
}
|
||||
warnNodeDeprecations();
|
||||
|
||||
validate(args);
|
||||
|
||||
if (args.tmpDir) {
|
||||
await validateTmpDir(args.tmpDir);
|
||||
}
|
||||
@@ -112,7 +110,7 @@ function logServerPort (address, port) {
|
||||
*
|
||||
* @param {Object} args - argparser parsed dict
|
||||
* @param {import('./plugin-config').default} pluginConfig - a plugin extension config
|
||||
* @returns {({pluginName: string} & ((...args: any[]) => unknown))[]}
|
||||
* @returns {PluginExtensionClass[]}
|
||||
*/
|
||||
function getActivePlugins (args, pluginConfig) {
|
||||
return _.compact(Object.keys(pluginConfig.installedExtensions).filter((pluginName) =>
|
||||
@@ -121,7 +119,7 @@ function getActivePlugins (args, pluginConfig) {
|
||||
).map((pluginName) => {
|
||||
try {
|
||||
logger.info(`Attempting to load plugin ${pluginName}...`);
|
||||
const PluginClass = /** @type {{pluginName: string} & ((...args: any[]) => unknown)} */(pluginConfig.require(pluginName));
|
||||
const PluginClass = /** @type {PluginExtensionClass} */(pluginConfig.require(pluginName));
|
||||
|
||||
PluginClass.pluginName = pluginName; // store the plugin name on the class so it can be used later
|
||||
return PluginClass;
|
||||
@@ -156,10 +154,22 @@ function getActiveDrivers (args, driverConfig) {
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of `updateServer` functions from all extensions
|
||||
* @param {DriverExtensionClass[]} driverClasses
|
||||
* @param {PluginExtensionClass[]} pluginClasses
|
||||
* @returns {StaticExtMembers['updateServer'][]}
|
||||
*/
|
||||
function getServerUpdaters (driverClasses, pluginClasses) {
|
||||
return [...driverClasses, ...pluginClasses].map((klass) => klass.updateServer).filter(Boolean);
|
||||
return _.compact(_.map([...driverClasses, ...pluginClasses], 'updateServer'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a big `MethodMap` from all the little `MethodMap`s in the extensions
|
||||
* @param {DriverExtensionClass[]} driverClasses
|
||||
* @param {PluginExtensionClass[]} pluginClasses
|
||||
* @returns {import('@appium/base-driver').MethodMap}
|
||||
*/
|
||||
function getExtraMethodMap (driverClasses, pluginClasses) {
|
||||
return [...driverClasses, ...pluginClasses].reduce(
|
||||
(map, klass) => ({...map, ...klass.newMethodMap}),
|
||||
@@ -175,20 +185,27 @@ function getExtraMethodMap (driverClasses, pluginClasses) {
|
||||
* If `args` contains a non-empty `subcommand` which is not `server`, this function
|
||||
* will resolve with an empty object.
|
||||
*
|
||||
* @todo: Use generics/conditional types to specify return values.
|
||||
* @param {ParsedArgs} [args] - Parsed args
|
||||
* @returns {Promise<Partial<{appiumDriver: AppiumDriver, parsedArgs: ParsedArgs}>>}
|
||||
* @example
|
||||
* import {init, getSchema} from 'appium';
|
||||
* const options = {}; // config object
|
||||
* await init(options);
|
||||
* const schema = getSchema(); // entire config schema including plugins and drivers
|
||||
* @param {ParsedArgs} [args] - Parsed args
|
||||
* @returns {Promise<Partial<{appiumDriver: AppiumDriver, parsedArgs: ParsedArgs}>>}
|
||||
*/
|
||||
async function init (args) {
|
||||
const parser = await getParser();
|
||||
let throwInsteadOfExit = false;
|
||||
/** @type {ParsedArgs} */
|
||||
let preConfigParsedArgs;
|
||||
/** @type {typeof preConfigParsedArgs & import('type-fest').AsyncReturnType<readConfigFile>['config']} */
|
||||
let parsedArgs;
|
||||
/**
|
||||
* This is a definition (instead of declaration) because TS can't figure out
|
||||
* the value will be defined when it's used.
|
||||
* @type {ReturnType<getDefaultsForSchema>}
|
||||
*/
|
||||
let defaults = {};
|
||||
if (args) {
|
||||
// if we have a containing package instead of running as a CLI process,
|
||||
// that package might not appreciate us calling 'process.exit' willy-
|
||||
@@ -198,29 +215,39 @@ async function init (args) {
|
||||
// but remove it since it's not a real server arg per se
|
||||
delete args.throwInsteadOfExit;
|
||||
}
|
||||
parsedArgs = {...args, subcommand: args.subcommand ?? SERVER_SUBCOMMAND};
|
||||
preConfigParsedArgs = {...args, subcommand: args.subcommand ?? SERVER_SUBCOMMAND};
|
||||
} else {
|
||||
// otherwise parse from CLI
|
||||
parsedArgs = parser.parseArgs();
|
||||
preConfigParsedArgs = parser.parseArgs();
|
||||
}
|
||||
|
||||
const configResult = await readConfigFile(parsedArgs.configFile);
|
||||
const configResult = await readConfigFile(preConfigParsedArgs.configFile);
|
||||
|
||||
if (!_.isEmpty(configResult.errors)) {
|
||||
throw new Error(`Errors in config file ${configResult.filepath}:\n ${configResult.reason ?? configResult.errors}`);
|
||||
}
|
||||
|
||||
|
||||
// merge config and apply defaults.
|
||||
// the order of precendece is:
|
||||
// 1. command line args
|
||||
// 2. config file
|
||||
// 3. defaults from config file.
|
||||
if (parsedArgs.subcommand === SERVER_SUBCOMMAND) {
|
||||
if (preConfigParsedArgs.subcommand === SERVER_SUBCOMMAND) {
|
||||
defaults = getDefaultsForSchema(false);
|
||||
|
||||
if (preConfigParsedArgs.showConfig) {
|
||||
showConfig(preConfigParsedArgs, configResult, defaults);
|
||||
return {};
|
||||
}
|
||||
|
||||
parsedArgs = _.defaultsDeep(
|
||||
parsedArgs,
|
||||
preConfigParsedArgs,
|
||||
configResult.config?.server,
|
||||
getDefaultsForSchema(false)
|
||||
defaults
|
||||
);
|
||||
} else {
|
||||
parsedArgs = preConfigParsedArgs;
|
||||
}
|
||||
|
||||
await logsinkInit(parsedArgs);
|
||||
@@ -310,7 +337,6 @@ async function main (args) {
|
||||
'to visit sites which could maliciously try to start Appium ' +
|
||||
'sessions on your machine');
|
||||
}
|
||||
// @ts-ignore
|
||||
appiumDriver.server = server;
|
||||
try {
|
||||
// configure as node on grid, if necessary
|
||||
@@ -363,3 +389,15 @@ export { readConfigFile } from './config-file';
|
||||
/**
|
||||
* @typedef {import('../types/types').ParsedArgs} ParsedArgs
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {import('./appium').PluginExtensionClass} PluginExtensionClass
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {import('./appium').DriverExtensionClass} DriverExtensionClass
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {import('./appium').StaticExtMembers} StaticExtMembers
|
||||
*/
|
||||
|
||||
@@ -98,11 +98,9 @@ function makeDescription (schema) {
|
||||
* as understood by `argparse`.
|
||||
* @param {AppiumJSONSchema} subSchema - JSON schema for the option
|
||||
* @param {ArgSpec} argSpec - Argument spec tuple
|
||||
* @param {SubSchemaToArgDefOptions} [opts] - Options
|
||||
* @returns {[string[], import('argparse').ArgumentOptions]} Tuple of flag and options
|
||||
*/
|
||||
function subSchemaToArgDef (subSchema, argSpec, opts = {}) {
|
||||
const {overrides = {}} = opts;
|
||||
function subSchemaToArgDef (subSchema, argSpec) {
|
||||
let {
|
||||
type,
|
||||
appiumCliAliases,
|
||||
@@ -110,7 +108,7 @@ function subSchemaToArgDef (subSchema, argSpec, opts = {}) {
|
||||
enum: enumValues,
|
||||
} = subSchema;
|
||||
|
||||
const {name, arg, dest} = argSpec;
|
||||
const {name, arg} = argSpec;
|
||||
|
||||
const aliases = [
|
||||
aliasToFlag(argSpec),
|
||||
@@ -142,7 +140,8 @@ function subSchemaToArgDef (subSchema, argSpec, opts = {}) {
|
||||
switch (type) {
|
||||
// booleans do not have a type per `ArgumentOptions`, just an "action"
|
||||
case TYPENAMES.BOOLEAN: {
|
||||
argOpts.action = 'store_true';
|
||||
argOpts.action = 'store_const';
|
||||
argOpts.const = true;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -227,16 +226,6 @@ function subSchemaToArgDef (subSchema, argSpec, opts = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
// overrides override anything we computed here. usually this involves "custom types",
|
||||
// which are really just transform functions.
|
||||
argOpts = _.merge(
|
||||
argOpts,
|
||||
/** should the override keys correspond to the prop name or the prop dest?
|
||||
* the prop dest is computed by {@link aliasToDest}.
|
||||
*/
|
||||
overrides[dest] ?? {},
|
||||
);
|
||||
|
||||
return [aliases, argOpts];
|
||||
}
|
||||
|
||||
@@ -244,32 +233,19 @@ function subSchemaToArgDef (subSchema, argSpec, opts = {}) {
|
||||
* Converts the finalized, flattened schema representation into
|
||||
* ArgumentDefinitions for handoff to `argparse`.
|
||||
*
|
||||
* @param {ToParserArgsOptions} opts - Options
|
||||
* @throws If schema has not been added to ajv (via `finalizeSchema()`)
|
||||
* @returns {import('../cli/args').ArgumentDefinitions} A map of arryas of
|
||||
* aliases to `argparse` arguments; empty if no schema found
|
||||
*/
|
||||
export function toParserArgs (opts = {}) {
|
||||
export function toParserArgs () {
|
||||
const flattened = flattenSchema().filter(({schema}) => !schema.appiumCliIgnored);
|
||||
return new Map(
|
||||
_.map(flattened, ({schema, argSpec}) =>
|
||||
subSchemaToArgDef(schema, argSpec, opts),
|
||||
subSchemaToArgDef(schema, argSpec),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for {@link toParserArgs}
|
||||
* @typedef {SubSchemaToArgDefOptions} ToParserArgsOptions
|
||||
*/
|
||||
|
||||
/**
|
||||
* Options for {@link subSchemaToArgDef}.
|
||||
* @typedef {Object} SubSchemaToArgDefOptions
|
||||
* @property {string} [prefix] - The prefix to use for the flag, if any
|
||||
* @property {{[key: string]: import('argparse').ArgumentOptions}} [overrides] - An object of key/value pairs to override the default values
|
||||
*/
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @typedef {import('ajv/dist/types').FormatValidator<T>} FormatValidator<T>
|
||||
|
||||
@@ -143,6 +143,14 @@ export const installLocalExtension = _.curry(
|
||||
* @typedef {Error & AppiumRunErrorProps & import('../../lib/cli/npm').TeenProcessExecErrorProps} AppiumRunError
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {import('../../lib/cli/npm').TeenProcessExecResult} TeenProcessExecResult
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {import('../../lib/cli/npm').TeenProcessExecError} TeenProcessExecError
|
||||
*/
|
||||
|
||||
/**
|
||||
* Wraps the error returned by {@link exec}.
|
||||
* @typedef {Object} AppiumRunErrorProps
|
||||
|
||||
@@ -1,22 +1,61 @@
|
||||
// @ts-check
|
||||
|
||||
// transpile:mocha
|
||||
import _ from 'lodash';
|
||||
import sinon from 'sinon';
|
||||
import getParser from '../lib/cli/parser';
|
||||
import { checkNodeOk, getBuildInfo, getNonDefaultServerArgs, showConfig, validateTmpDir, warnNodeDeprecations } from '../lib/config';
|
||||
import { checkNodeOk, getBuildInfo, getNonDefaultServerArgs, showBuildInfo, showConfig, validateTmpDir, warnNodeDeprecations } from '../lib/config';
|
||||
import logger from '../lib/logger';
|
||||
import { getDefaultsForSchema, resetSchema, registerSchema, finalizeSchema } from '../lib/schema/schema';
|
||||
|
||||
describe('Config', function () {
|
||||
/** @type {import('sinon').SinonSandbox} */
|
||||
let sandbox;
|
||||
|
||||
beforeEach(function () {
|
||||
sandbox = sinon.createSandbox();
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
describe('Appium config', function () {
|
||||
describe('showConfig', function () {
|
||||
before(function () {
|
||||
sinon.spy(console, 'log');
|
||||
});
|
||||
it('should log the config to console', async function () {
|
||||
/** @type {import('sinon').SinonSpy<[message?: any, ...extra: any[]],void>} */
|
||||
let log;
|
||||
/** @type {import('sinon').SinonSpy<[message?: any, ...extra: any[]],void>} */
|
||||
let dir;
|
||||
beforeEach(function () {
|
||||
log = sandbox.spy(console, 'log');
|
||||
dir = sandbox.spy(console, 'dir');
|
||||
});
|
||||
|
||||
describe('showBuildInfo()', function () {
|
||||
it('should log build info to console', async function () {
|
||||
const config = getBuildInfo();
|
||||
await showConfig();
|
||||
console.log.calledOnce.should.be.true; // eslint-disable-line no-console
|
||||
console.log.getCall(0).args[0].should.contain(JSON.stringify(config)); // eslint-disable-line no-console
|
||||
await showBuildInfo();
|
||||
log.should.have.been.calledOnce;
|
||||
log.firstCall.args.should.contain(JSON.stringify(config));
|
||||
});
|
||||
});
|
||||
|
||||
describe('showConfig', function () {
|
||||
describe('when a config file is present', function () {
|
||||
it('should dump the current Appium config', function () {
|
||||
showConfig({foo: 'bar'}, {config: {server: {address: 'quux'}}}, {spam: 'food'});
|
||||
log.should.have.been.calledWith('Appium Configuration\n');
|
||||
});
|
||||
|
||||
it('should skip empty objects', function () {
|
||||
showConfig({foo: 'bar', cows: {}, pigs: [], sheep: 0, ducks: false}, {config: {server: {address: 'quux'}}}, {spam: 'food'});
|
||||
dir.should.have.been.calledWith({foo: 'bar', sheep: 0, ducks: false});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when a config file is not present', function () {
|
||||
it('should dump the current Appium config sans config file contents', function () {
|
||||
showConfig({foo: 'bar'}, {}, {spam: 'food'});
|
||||
log.should.have.been.calledWith('(no configuration file loaded)\n');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -27,11 +66,7 @@ describe('Config', function () {
|
||||
// need to be able to write to process.version
|
||||
// but also to have access to process methods
|
||||
// so copy them over to a writable object
|
||||
let tempProcess = {};
|
||||
for (let [prop, value] of _.toPairs(process)) {
|
||||
tempProcess[prop] = value;
|
||||
}
|
||||
process = tempProcess; // eslint-disable-line no-global-assign
|
||||
process = {...process}; // eslint-disable-line no-global-assign
|
||||
});
|
||||
after(function () {
|
||||
process = _process; // eslint-disable-line no-global-assign
|
||||
@@ -45,6 +80,7 @@ describe('Config', function () {
|
||||
];
|
||||
for (const version of unsupportedVersions) {
|
||||
it(`should fail if node is ${version}`, function () {
|
||||
// @ts-expect-error
|
||||
process.version = version;
|
||||
checkNodeOk.should.throw();
|
||||
});
|
||||
@@ -53,14 +89,17 @@ describe('Config', function () {
|
||||
|
||||
describe('supported nodes', function () {
|
||||
it('should succeed if node is 12+', function () {
|
||||
// @ts-expect-error
|
||||
process.version = 'v12.0.1';
|
||||
checkNodeOk.should.not.throw();
|
||||
});
|
||||
it('should succeed if node is 13+', function () {
|
||||
// @ts-expect-error
|
||||
process.version = 'v13.6.0';
|
||||
checkNodeOk.should.not.throw();
|
||||
});
|
||||
it('should succeed if node is 14+', function () {
|
||||
// @ts-expect-error
|
||||
process.version = 'v14.0.0';
|
||||
checkNodeOk.should.not.throw();
|
||||
});
|
||||
@@ -70,17 +109,19 @@ describe('Config', function () {
|
||||
describe('warnNodeDeprecations', function () {
|
||||
let spy;
|
||||
before(function () {
|
||||
spy = sinon.spy(logger, 'warn');
|
||||
spy = sandbox.spy(logger, 'warn');
|
||||
});
|
||||
beforeEach(function () {
|
||||
spy.resetHistory();
|
||||
});
|
||||
it('should not log a warning if node is 8+', function () {
|
||||
// @ts-expect-error
|
||||
process.version = 'v8.0.0';
|
||||
warnNodeDeprecations();
|
||||
logger.warn.callCount.should.equal(0);
|
||||
});
|
||||
it('should not log a warning if node is 9+', function () {
|
||||
// @ts-expect-error
|
||||
process.version = 'v9.0.0';
|
||||
warnNodeDeprecations();
|
||||
logger.warn.callCount.should.equal(0);
|
||||
@@ -142,6 +183,7 @@ describe('Config', function () {
|
||||
validateTmpDir('/private/if_you_run_with_sudo_this_wont_fail').should.be.rejectedWith(/could not ensure/);
|
||||
});
|
||||
it('should fail to use an undefined tmp dir', function () {
|
||||
// @ts-expect-error
|
||||
validateTmpDir().should.be.rejectedWith(/could not ensure/);
|
||||
});
|
||||
it('should be able to use a tmp dir with correct permissions', function () {
|
||||
@@ -165,7 +207,7 @@ describe('Config', function () {
|
||||
});
|
||||
|
||||
it('should not fail if process.argv[1] is undefined', async function () {
|
||||
process.argv[1] = undefined;
|
||||
delete process.argv[1];
|
||||
let args = await getParser();
|
||||
args.prog.should.be.equal('appium');
|
||||
});
|
||||
|
||||
@@ -94,7 +94,7 @@ describe('parser', function () {
|
||||
});
|
||||
|
||||
it('should parse --allow-insecure correctly', function () {
|
||||
p.parseArgs([]).should.have.property('allowInsecure', undefined);
|
||||
p.parseArgs([]).should.not.have.property('allowInsecure');
|
||||
p.parseArgs(['--allow-insecure', '']).allowInsecure.should.eql([]);
|
||||
p.parseArgs(['--allow-insecure', 'foo']).allowInsecure.should.eql(['foo']);
|
||||
p.parseArgs(['--allow-insecure', 'foo,bar']).allowInsecure.should.eql(['foo', 'bar']);
|
||||
@@ -102,7 +102,7 @@ describe('parser', function () {
|
||||
});
|
||||
|
||||
it('should parse --deny-insecure correctly', function () {
|
||||
p.parseArgs([]).should.have.property('denyInsecure', undefined);
|
||||
p.parseArgs([]).should.not.have.property('denyInsecure');
|
||||
p.parseArgs(['--deny-insecure', '']).denyInsecure.should.eql([]);
|
||||
p.parseArgs(['--deny-insecure', 'foo']).denyInsecure.should.eql(['foo']);
|
||||
p.parseArgs(['--deny-insecure', 'foo,bar']).denyInsecure.should.eql(['foo', 'bar']);
|
||||
@@ -161,7 +161,7 @@ describe('parser', function () {
|
||||
|
||||
it('should not yet apply defaults', function () {
|
||||
const args = p.parseArgs([]);
|
||||
args.driver.fake.should.eql({sillyWebServerHost: undefined, sillyWebServerPort: undefined});
|
||||
args.should.not.have.property('driver');
|
||||
});
|
||||
|
||||
it('should nicely handle extensions w/ dashes in them', async function () {
|
||||
|
||||
@@ -20,7 +20,7 @@ describe('cli-args', function () {
|
||||
registerSchema(extType, extName, schema);
|
||||
}
|
||||
finalizeSchema();
|
||||
return _.fromPairs([...toParserArgs(opts)]);
|
||||
return _.fromPairs([...toParserArgs()]);
|
||||
}
|
||||
|
||||
beforeEach(resetSchema);
|
||||
@@ -40,10 +40,10 @@ describe('cli-args', function () {
|
||||
result = getArgs({schema, extName, extType});
|
||||
});
|
||||
|
||||
it('should return options containing `action` prop of `store_true` and no `type`', function () {
|
||||
it('should return options containing `action` prop of `store_const` and no `type`', function () {
|
||||
expect(result['--plugin-blob-foo']).to.have.property(
|
||||
'action',
|
||||
'store_true',
|
||||
'store_const',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -343,32 +343,6 @@ describe('cli-args', function () {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('overrides', function () {
|
||||
// this might be better some other way, IDK. I suspect this will be rarely used.
|
||||
// since extensions can't actually call `toParserArgs()`, they can't set `overrides` anyway.
|
||||
it('should set them via "dest" key', function () {
|
||||
const schema = {
|
||||
properties: {
|
||||
foo: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
type: 'object',
|
||||
};
|
||||
|
||||
const result = getArgs({
|
||||
schema,
|
||||
extName,
|
||||
extType,
|
||||
overrides: {'plugin.blob.foo': {enum: ['slug', 'snail']}},
|
||||
});
|
||||
expect(result['--plugin-blob-foo']).to.have.deep.property('enum', [
|
||||
'slug',
|
||||
'snail',
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Vendored
+5
@@ -160,6 +160,11 @@ interface MoreArgs {
|
||||
/**
|
||||
* If true, show the build info and exit
|
||||
*/
|
||||
showBuildInfo: boolean;
|
||||
|
||||
/**
|
||||
* If true, show config and exit
|
||||
*/
|
||||
showConfig: boolean;
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user