appium 2.0.0-beta.17 → 2.0.0-beta.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/build/lib/appium-config.schema.json +0 -0
  2. package/build/lib/appium.js +84 -69
  3. package/build/lib/cli/argparse-actions.js +1 -1
  4. package/build/lib/cli/args.js +87 -223
  5. package/build/lib/cli/extension-command.js +2 -2
  6. package/build/lib/cli/extension.js +14 -6
  7. package/build/lib/cli/parser.js +142 -106
  8. package/build/lib/cli/utils.js +1 -1
  9. package/build/lib/config-file.js +141 -0
  10. package/build/lib/config.js +42 -64
  11. package/build/lib/driver-config.js +41 -20
  12. package/build/lib/drivers.js +1 -1
  13. package/build/lib/ext-config-io.js +165 -0
  14. package/build/lib/extension-config.js +110 -60
  15. package/build/lib/grid-register.js +19 -21
  16. package/build/lib/logsink.js +1 -1
  17. package/build/lib/main.js +135 -72
  18. package/build/lib/plugin-config.js +17 -8
  19. package/build/lib/schema/appium-config-schema.js +252 -0
  20. package/build/lib/schema/arg-spec.js +120 -0
  21. package/build/lib/schema/cli-args.js +173 -0
  22. package/build/lib/schema/cli-transformers.js +76 -0
  23. package/build/lib/schema/index.js +36 -0
  24. package/build/lib/schema/keywords.js +62 -0
  25. package/build/lib/schema/schema.js +357 -0
  26. package/build/lib/utils.js +26 -35
  27. package/lib/appium-config.schema.json +277 -0
  28. package/lib/appium.js +99 -75
  29. package/lib/cli/args.js +138 -335
  30. package/lib/cli/extension-command.js +7 -6
  31. package/lib/cli/extension.js +12 -4
  32. package/lib/cli/parser.js +248 -96
  33. package/lib/config-file.js +227 -0
  34. package/lib/config.js +71 -61
  35. package/lib/driver-config.js +66 -11
  36. package/lib/ext-config-io.js +287 -0
  37. package/lib/extension-config.js +209 -66
  38. package/lib/grid-register.js +24 -21
  39. package/lib/main.js +139 -68
  40. package/lib/plugin-config.js +32 -2
  41. package/lib/schema/appium-config-schema.js +286 -0
  42. package/lib/schema/arg-spec.js +218 -0
  43. package/lib/schema/cli-args.js +273 -0
  44. package/lib/schema/cli-transformers.js +123 -0
  45. package/lib/schema/index.js +2 -0
  46. package/lib/schema/keywords.js +119 -0
  47. package/lib/schema/schema.js +577 -0
  48. package/lib/utils.js +29 -52
  49. package/package.json +16 -11
  50. package/types/appium-config.d.ts +197 -0
  51. package/types/types.d.ts +201 -0
  52. package/build/lib/cli/parser-helpers.js +0 -106
  53. package/lib/cli/parser-helpers.js +0 -106
@@ -1,16 +1,15 @@
1
+ // @ts-check
2
+
1
3
  import _ from 'lodash';
2
- import log from './logger';
3
- import { fs, mkdirp } from '@appium/support';
4
- import path from 'path';
5
4
  import os from 'os';
6
- import YAML from 'yaml';
5
+ import path from 'path';
6
+ import resolveFrom from 'resolve-from';
7
+ import { getExtConfigIOInstance } from './ext-config-io';
8
+ import log from './logger';
9
+ import { ALLOWED_SCHEMA_EXTENSIONS, isAllowedSchemaFileExtension, registerSchema } from './schema/schema';
7
10
 
8
- const DRIVER_TYPE = 'driver';
9
- const PLUGIN_TYPE = 'plugin';
10
11
  const DEFAULT_APPIUM_HOME = path.resolve(os.homedir(), '.appium');
11
-
12
- const CONFIG_FILE_NAME = 'extensions.yaml';
13
- const CONFIG_SCHEMA_REV = 2;
12
+ const APPIUM_HOME = process.env.APPIUM_HOME || DEFAULT_APPIUM_HOME;
14
13
 
15
14
  const INSTALL_TYPE_NPM = 'npm';
16
15
  const INSTALL_TYPE_LOCAL = 'local';
@@ -23,27 +22,44 @@ const INSTALL_TYPES = [
23
22
  INSTALL_TYPE_NPM
24
23
  ];
25
24
 
26
-
27
25
  export default class ExtensionConfig {
28
- constructor (appiumHome, extensionType, logFn = null) {
29
- if (logFn === null) {
30
- logFn = log.error.bind(log);
31
- }
26
+ /**
27
+ *
28
+ * @param {string} appiumHome - `APPIUM_HOME`
29
+ * @param {ExtensionType} extensionType - Type of extension
30
+ * @param {(...args: any[]) => void} [logFn]
31
+ */
32
+ constructor (appiumHome, extensionType, logFn) {
33
+ const logger = _.isFunction(logFn) ? logFn : log.error.bind(log);
34
+ /** @type {string} */
32
35
  this.appiumHome = appiumHome;
33
- this.configFile = path.resolve(this.appiumHome, CONFIG_FILE_NAME);
36
+ /** @type {Record<string,object>} */
34
37
  this.installedExtensions = {};
38
+ /** @type {import('./ext-config-io').ExtensionConfigIO} */
39
+ this.io = getExtConfigIOInstance(appiumHome);
40
+ /** @type {ExtensionType} */
35
41
  this.extensionType = extensionType;
36
- this.configKey = `${extensionType}s`;
37
- this.yamlData = {[`${DRIVER_TYPE}s`]: {}, [`${PLUGIN_TYPE}s`]: {}};
38
- this.log = logFn;
42
+ /** @type {'drivers'|'plugins'} */
43
+ this.configKey = `${extensionType}s`; // todo use template type
44
+ /**
45
+ * @type {(...args: any[])=>void}
46
+ */
47
+ this.log = logger;
39
48
  }
40
49
 
50
+ /**
51
+ * Checks extensions for problems
52
+ * @template ExtData
53
+ * @param {ExtData[]} exts - Array of extData objects
54
+ * @returns {ExtData[]}
55
+ */
41
56
  validate (exts) {
42
57
  const foundProblems = {};
43
58
  for (const [extName, extData] of _.toPairs(exts)) {
44
59
  foundProblems[extName] = [
45
- ...this.getGenericConfigProblems(extData),
46
- ...this.getConfigProblems(extData)
60
+ ...this.getGenericConfigProblems(extData, extName),
61
+ ...this.getConfigProblems(extData, extName),
62
+ ...this.getSchemaProblems(extData, extName)
47
63
  ];
48
64
  }
49
65
 
@@ -64,7 +80,7 @@ export default class ExtensionConfig {
64
80
 
65
81
  if (!_.isEmpty(problemSummaries)) {
66
82
  this.log(`Appium encountered one or more errors while validating ` +
67
- `the ${this.configKey} extension file (${this.configFile}):`);
83
+ `the ${this.configKey} extension file (${this.io.filepath}):`);
68
84
  for (const summary of problemSummaries) {
69
85
  this.log(summary);
70
86
  }
@@ -73,8 +89,52 @@ export default class ExtensionConfig {
73
89
  return exts;
74
90
  }
75
91
 
76
- getGenericConfigProblems (ext) {
77
- const {version, pkgName, installSpec, installType, installPath, mainClass} = ext;
92
+ /**
93
+ * @param {object} extData
94
+ * @param {string} extName
95
+ * @returns {Problem[]}
96
+ */
97
+ getSchemaProblems (extData, extName) {
98
+ const problems = [];
99
+ const {schema: argSchemaPath} = extData;
100
+ if (argSchemaPath) {
101
+ if (_.isString(argSchemaPath)) {
102
+ if (isAllowedSchemaFileExtension(argSchemaPath)) {
103
+ try {
104
+ this.readExtensionSchema(extName, extData);
105
+ } catch (err) {
106
+ problems.push({err: `Unable to register schema at path ${argSchemaPath}; ${err.message}`, val: argSchemaPath});
107
+ }
108
+ } else {
109
+ problems.push({
110
+ err: `Schema file has unsupported extension. Allowed: ${[...ALLOWED_SCHEMA_EXTENSIONS].join(', ')}`,
111
+ val: argSchemaPath
112
+ });
113
+ }
114
+ } else if (_.isPlainObject(argSchemaPath)) {
115
+ try {
116
+ this.readExtensionSchema(extName, extData);
117
+ } catch (err) {
118
+ problems.push({err: `Unable to register embedded schema; ${err.message}`, val: argSchemaPath});
119
+ }
120
+ } else {
121
+ problems.push({
122
+ err: 'Incorrectly formatted schema field; must be a path to a schema file or a schema object.',
123
+ val: argSchemaPath
124
+ });
125
+ }
126
+ }
127
+ return problems;
128
+ }
129
+
130
+ /**
131
+ * @param {object} extData
132
+ * @param {string} extName
133
+ * @returns {Problem[]}
134
+ */
135
+ // eslint-disable-next-line no-unused-vars
136
+ getGenericConfigProblems (extData, extName) {
137
+ const {version, pkgName, installSpec, installType, installPath, mainClass} = extData;
78
138
  const problems = [];
79
139
 
80
140
  if (!_.isString(version)) {
@@ -104,62 +164,48 @@ export default class ExtensionConfig {
104
164
  return problems;
105
165
  }
106
166
 
107
- getConfigProblems (/*ext*/) {
167
+ /**
168
+ * @param {object} extData
169
+ * @param {string} extName
170
+ * @returns {Problem[]}
171
+ */
172
+ // eslint-disable-next-line no-unused-vars
173
+ getConfigProblems (extData, extName) {
108
174
  // shoud override this method if special validation is necessary for this extension type
109
175
  return [];
110
176
  }
111
177
 
112
- applySchemaMigrations () {
113
- if (this.yamlData.schemaRev < 2 && _.isUndefined(this.yamlData[PLUGIN_TYPE])) {
114
- // at schema revision 2, we started including plugins as well as drivers in the file,
115
- // so make sure we at least have an empty section for it
116
- this.yamlData[PLUGIN_TYPE] = {};
117
- }
118
- }
119
-
178
+ /**
179
+ * @returns {Promise<typeof this.installedExtensions>}
180
+ */
120
181
  async read () {
121
- await mkdirp(this.appiumHome); // ensure appium home exists
122
- try {
123
- this.yamlData = YAML.parse(await fs.readFile(this.configFile, 'utf8'));
124
- this.applySchemaMigrations();
125
-
126
- // set the list of drivers the user has installed
127
- this.installedExtensions = this.validate(this.yamlData[this.configKey]);
128
- } catch (err) {
129
- if (await fs.exists(this.configFile)) {
130
- // if the file exists and we couldn't parse it, that's a problem
131
- throw new Error(`Appium had trouble loading the extension installation ` +
132
- `cache file (${this.configFile}). Ensure it exists and is ` +
133
- `readable. Specific error: ${err.message}`);
134
- }
135
-
136
- // if the config file doesn't exist, try to write an empty one, to make
137
- // sure we actually have write privileges, and complain if we don't
138
- try {
139
- await this.write();
140
- } catch {
141
- throw new Error(`Appium could not read or write from the Appium Home directory ` +
142
- `(${this.appiumHome}). Please ensure it is writable.`);
143
- }
144
- }
182
+ const extensions = await this.io.read(this.extensionType);
183
+ this.installedExtensions = this.validate(extensions);
145
184
  return this.installedExtensions;
146
185
  }
147
186
 
148
-
187
+ /**
188
+ * @returns {Promise<boolean>}
189
+ */
149
190
  async write () {
150
- const newYamlData = {
151
- ...this.yamlData,
152
- schemaRev: CONFIG_SCHEMA_REV,
153
- [this.configKey]: this.installedExtensions
154
- };
155
- await fs.writeFile(this.configFile, YAML.stringify(newYamlData), 'utf8');
191
+ return await this.io.write();
156
192
  }
157
193
 
194
+ /**
195
+ * @param {string} extName
196
+ * @param {object} extData
197
+ * @returns {Promise<void>}
198
+ */
158
199
  async addExtension (extName, extData) {
159
200
  this.installedExtensions[extName] = extData;
160
201
  await this.write();
161
202
  }
162
203
 
204
+ /**
205
+ * @param {string} extName
206
+ * @param {object} extData
207
+ * @returns {Promise<void>}
208
+ */
163
209
  async updateExtension (extName, extData) {
164
210
  this.installedExtensions[extName] = {
165
211
  ...this.installedExtensions[extName],
@@ -168,6 +214,10 @@ export default class ExtensionConfig {
168
214
  await this.write();
169
215
  }
170
216
 
217
+ /**
218
+ * @param {string} extName
219
+ * @returns {Promise<void>}
220
+ */
171
221
  async removeExtension (extName) {
172
222
  delete this.installedExtensions[extName];
173
223
  await this.write();
@@ -187,20 +237,41 @@ export default class ExtensionConfig {
187
237
  }
188
238
  }
189
239
 
190
- extensionDesc () {
191
- throw new Error('This must be implemented in a final class');
240
+ /**
241
+ * Returns a string describing the extension. Subclasses must implement.
242
+ * @param {string} extName - Extension name
243
+ * @param {object} extData - Extension data
244
+ * @returns {string}
245
+ * @abstract
246
+ */
247
+ // eslint-disable-next-line no-unused-vars
248
+ extensionDesc (extName, extData) {
249
+ throw new Error('This must be implemented in a subclass');
192
250
  }
193
251
 
252
+ /**
253
+ * @param {string} extName
254
+ * @returns {string}
255
+ */
194
256
  getExtensionRequirePath (extName) {
195
257
  const {pkgName, installPath} = this.installedExtensions[extName];
196
258
  return path.resolve(this.appiumHome, installPath, 'node_modules', pkgName);
197
259
  }
198
260
 
261
+ /**
262
+ * @param {string} extName
263
+ * @returns {string}
264
+ */
199
265
  getInstallPath (extName) {
200
266
  const {installPath} = this.installedExtensions[extName];
201
267
  return path.resolve(this.appiumHome, installPath);
202
268
  }
203
269
 
270
+ /**
271
+ * Loads extension and returns its main class
272
+ * @param {string} extName
273
+ * @returns {(...args: any[]) => object }
274
+ */
204
275
  require (extName) {
205
276
  const {mainClass} = this.installedExtensions[extName];
206
277
  const reqPath = this.getExtensionRequirePath(extName);
@@ -212,12 +283,84 @@ export default class ExtensionConfig {
212
283
  return require(reqPath)[mainClass];
213
284
  }
214
285
 
286
+ /**
287
+ * @param {string} extName
288
+ * @returns {boolean}
289
+ */
215
290
  isInstalled (extName) {
216
291
  return _.includes(Object.keys(this.installedExtensions), extName);
217
292
  }
293
+
294
+ /**
295
+ * Intended to be called by corresponding instance methods of subclass.
296
+ * @private
297
+ * @param {string} appiumHome
298
+ * @param {ExtensionType} extType
299
+ * @param {string} extName - Extension name (unique to its type)
300
+ * @param {ExtData} extData - Extension config
301
+ * @returns {import('ajv').SchemaObject|undefined}
302
+ */
303
+ static _readExtensionSchema (appiumHome, extType, extName, extData) {
304
+ const {installPath, pkgName, schema: argSchemaPath} = extData;
305
+ if (!argSchemaPath) {
306
+ throw new TypeError(
307
+ `No \`schema\` property found in config for ${extType} ${pkgName} -- why is this function being called?`,
308
+ );
309
+ }
310
+ let moduleObject;
311
+ if (_.isString(argSchemaPath)) {
312
+ const schemaPath = resolveFrom(
313
+ path.resolve(appiumHome, installPath),
314
+ // this path sep is fine because `resolveFrom` uses Node's module resolution
315
+ path.normalize(`${pkgName}/${argSchemaPath}`),
316
+ );
317
+ moduleObject = require(schemaPath);
318
+ } else {
319
+ moduleObject = argSchemaPath;
320
+ }
321
+ // this sucks. default exports should be destroyed
322
+ const schema = moduleObject.__esModule
323
+ ? moduleObject.default
324
+ : moduleObject;
325
+ registerSchema(extType, extName, schema);
326
+ return schema;
327
+ }
328
+
329
+ /**
330
+ * If an extension provides a schema, this will load the schema and attempt to
331
+ * register it with the schema registrar.
332
+ * @param {string} extName - Name of extension
333
+ * @param {ExtData} extData - Extension data
334
+ * @returns {import('ajv').SchemaObject|undefined}
335
+ */
336
+ readExtensionSchema (extName, extData) {
337
+ return ExtensionConfig._readExtensionSchema(this.appiumHome, this.extensionType, extName, extData);
338
+ }
218
339
  }
219
340
 
341
+ export { DRIVER_TYPE, PLUGIN_TYPE } from './ext-config-io';
220
342
  export {
221
343
  INSTALL_TYPE_NPM, INSTALL_TYPE_GIT, INSTALL_TYPE_LOCAL, INSTALL_TYPE_GITHUB,
222
- INSTALL_TYPES, DEFAULT_APPIUM_HOME, DRIVER_TYPE, PLUGIN_TYPE,
344
+ INSTALL_TYPES, DEFAULT_APPIUM_HOME, APPIUM_HOME
223
345
  };
346
+
347
+ /**
348
+ * Config problem
349
+ * @typedef {Object} Problem
350
+ * @property {string} err - Error message
351
+ * @property {any} val - Associated value
352
+ */
353
+
354
+ /**
355
+ * Alias
356
+ * @typedef {import('./ext-config-io').ExtensionType} ExtensionType
357
+ */
358
+
359
+ /**
360
+ * Extension data (pulled from config YAML)
361
+ * @typedef {Object} ExtData
362
+ * @property {string|import('ajv').SchemaObject} [schema] - Optional schema path if the ext defined it
363
+ * @property {string} pkgName - Package name
364
+ * @property {string} installPath - Actually looks more like a module identifier? Resolved from `APPIUM_HOME`
365
+ */
366
+
@@ -9,20 +9,31 @@ const hubUri = (config) => {
9
9
  return `${protocol}://${config.hubHost}:${config.hubPort}`;
10
10
  };
11
11
 
12
- async function registerNode (configFile, addr, port, basePath) {
13
- let data;
14
- try {
15
- data = await fs.readFile(configFile, 'utf-8');
16
- } catch (err) {
17
- logger.error(`Unable to load node configuration file to register with grid: ${err.message}`);
18
- return;
12
+ /**
13
+ * Registers a new node with a selenium grid
14
+ * @param {string|object} data - Path or object representing selenium grid node config file
15
+ * @param {string} addr - Bind to this address
16
+ * @param {number} port - Bind to this port
17
+ * @param {string} basePath - Base path for the grid
18
+ */
19
+ async function registerNode (data, addr, port, basePath) {
20
+ let configFilePath;
21
+ if (_.isString(data)) {
22
+ configFilePath = data;
23
+ try {
24
+ data = await fs.readFile(data, 'utf-8');
25
+ } catch (err) {
26
+ logger.error(`Unable to load node configuration file ${configFilePath} to register with grid: ${err.message}`);
27
+ return;
28
+ }
29
+ try {
30
+ data = JSON.parse(data);
31
+ } catch (err) {
32
+ logger.errorAndThrow(`Syntax error in node configuration file ${configFilePath}: ${err.message}`);
33
+ return;
34
+ }
19
35
  }
20
36
 
21
- // Check presence of data before posting it to the selenium grid
22
- if (!data) {
23
- logger.error('No data found in the node configuration file to send to the grid');
24
- return;
25
- }
26
37
  postRequest(data, addr, port, basePath);
27
38
  }
28
39
 
@@ -39,15 +50,7 @@ async function registerToGrid (postOptions, configHolder) {
39
50
  }
40
51
  }
41
52
 
42
- function postRequest (data, addr, port, basePath) {
43
- // parse json to get hub host and port
44
- let configHolder;
45
- try {
46
- configHolder = JSON.parse(data);
47
- } catch (err) {
48
- logger.errorAndThrow(`Syntax error in node configuration file: ${err.message}`);
49
- }
50
-
53
+ function postRequest (configHolder, addr, port, basePath) {
51
54
  // Move Selenium 3 configuration properties to configuration object
52
55
  if (!_.has(configHolder, 'configuration')) {
53
56
  let configuration = {};