appium 2.0.0-beta.2 → 2.0.0-beta.23
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.
- package/README.md +9 -9
- package/build/check-npm-pack-files.js +23 -0
- package/build/commands-yml/parse.js +319 -0
- package/build/commands-yml/validator.js +130 -0
- package/build/index.js +19 -0
- package/build/lib/appium-config.schema.json +0 -0
- package/build/lib/appium.js +160 -53
- package/build/lib/cli/args.js +115 -279
- package/build/lib/cli/driver-command.js +11 -1
- package/build/lib/cli/extension-command.js +60 -8
- package/build/lib/cli/extension.js +30 -7
- package/build/lib/cli/npm.js +43 -29
- package/build/lib/cli/parser.js +156 -89
- package/build/lib/cli/plugin-command.js +11 -1
- package/build/lib/cli/utils.js +29 -3
- package/build/lib/config-file.js +141 -0
- package/build/lib/config.js +53 -65
- package/build/lib/driver-config.js +42 -19
- package/build/lib/drivers.js +8 -4
- package/build/lib/ext-config-io.js +165 -0
- package/build/lib/extension-config.js +130 -61
- package/build/lib/grid-register.js +22 -24
- package/build/lib/logger.js +3 -3
- package/build/lib/logsink.js +11 -13
- package/build/lib/main.js +197 -77
- package/build/lib/plugin-config.js +21 -11
- package/build/lib/plugins.js +4 -2
- package/build/lib/schema/appium-config-schema.js +253 -0
- package/build/lib/schema/arg-spec.js +120 -0
- package/build/lib/schema/cli-args.js +188 -0
- package/build/lib/schema/cli-transformers.js +76 -0
- package/build/lib/schema/index.js +36 -0
- package/build/lib/schema/keywords.js +72 -0
- package/build/lib/schema/schema.js +357 -0
- package/build/lib/utils.js +44 -99
- package/build/postinstall.js +90 -0
- package/build/test/cli/cli-e2e-specs.js +221 -0
- package/build/test/cli/cli-helpers.js +86 -0
- package/build/test/cli/cli-specs.js +71 -0
- package/build/test/cli/fixtures/test-driver/package.json +27 -0
- package/build/test/cli/schema-args-specs.js +48 -0
- package/build/test/cli/schema-e2e-specs.js +47 -0
- package/build/test/config-e2e-specs.js +112 -0
- package/build/test/config-file-e2e-specs.js +209 -0
- package/build/test/config-file-specs.js +281 -0
- package/build/test/config-specs.js +159 -0
- package/build/test/driver-e2e-specs.js +435 -0
- package/build/test/driver-specs.js +321 -0
- package/build/test/ext-config-io-specs.js +181 -0
- package/build/test/extension-config-specs.js +365 -0
- package/build/test/fixtures/allow-feat.txt +5 -0
- package/build/test/fixtures/caps.json +3 -0
- package/build/test/fixtures/config/allow-insecure.txt +3 -0
- package/build/test/fixtures/config/appium.config.bad-nodeconfig.json +5 -0
- package/build/test/fixtures/config/appium.config.bad.json +32 -0
- package/build/test/fixtures/config/appium.config.ext-good.json +9 -0
- package/build/test/fixtures/config/appium.config.ext-unknown-props.json +10 -0
- package/build/test/fixtures/config/appium.config.good.js +40 -0
- package/build/test/fixtures/config/appium.config.good.json +33 -0
- package/build/test/fixtures/config/appium.config.good.yaml +30 -0
- package/build/test/fixtures/config/appium.config.invalid.json +31 -0
- package/build/test/fixtures/config/appium.config.security-array.json +5 -0
- package/build/test/fixtures/config/appium.config.security-delimited.json +5 -0
- package/build/test/fixtures/config/appium.config.security-path.json +5 -0
- package/build/test/fixtures/config/driver-fake.config.json +8 -0
- package/build/test/fixtures/config/nodeconfig.json +3 -0
- package/build/test/fixtures/config/plugin-fake.config.json +0 -0
- package/build/test/fixtures/default-args.js +35 -0
- package/build/test/fixtures/deny-feat.txt +5 -0
- package/build/test/fixtures/driver.schema.js +20 -0
- package/build/test/fixtures/extensions.yaml +27 -0
- package/build/test/fixtures/flattened-schema.js +504 -0
- package/build/test/fixtures/plugin.schema.js +20 -0
- package/build/test/fixtures/schema-with-extensions.js +28 -0
- package/build/test/grid-register-specs.js +74 -0
- package/build/test/helpers.js +75 -0
- package/build/test/logger-specs.js +76 -0
- package/build/test/npm-specs.js +20 -0
- package/build/test/parser-specs.js +314 -0
- package/build/test/plugin-e2e-specs.js +316 -0
- package/build/test/schema/arg-spec-specs.js +70 -0
- package/build/test/schema/cli-args-specs.js +431 -0
- package/build/test/schema/schema-specs.js +389 -0
- package/build/test/utils-specs.js +266 -0
- package/index.js +11 -0
- package/lib/appium-config.schema.json +278 -0
- package/lib/appium.js +207 -65
- package/lib/cli/args.js +174 -375
- package/lib/cli/driver-command.js +4 -0
- package/lib/cli/extension-command.js +70 -5
- package/lib/cli/extension.js +25 -5
- package/lib/cli/npm.js +86 -18
- package/lib/cli/parser.js +257 -79
- package/lib/cli/plugin-command.js +4 -0
- package/lib/cli/utils.js +21 -1
- package/lib/config-file.js +227 -0
- package/lib/config.js +84 -63
- package/lib/driver-config.js +66 -11
- package/lib/drivers.js +4 -1
- package/lib/ext-config-io.js +287 -0
- package/lib/extension-config.js +225 -67
- package/lib/grid-register.js +27 -24
- package/lib/logger.js +1 -1
- package/lib/logsink.js +10 -7
- package/lib/main.js +214 -77
- package/lib/plugin-config.js +35 -6
- package/lib/plugins.js +1 -0
- package/lib/schema/appium-config-schema.js +287 -0
- package/lib/schema/arg-spec.js +222 -0
- package/lib/schema/cli-args.js +285 -0
- package/lib/schema/cli-transformers.js +123 -0
- package/lib/schema/index.js +2 -0
- package/lib/schema/keywords.js +135 -0
- package/lib/schema/schema.js +577 -0
- package/lib/utils.js +42 -88
- package/package.json +55 -84
- package/postinstall.js +71 -0
- package/types/appium-config.d.ts +197 -0
- package/types/types.d.ts +201 -0
- package/CHANGELOG.md +0 -3515
- package/build/lib/cli/parser-helpers.js +0 -82
- package/lib/cli/parser-helpers.js +0 -79
package/lib/config.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import _ from 'lodash';
|
|
2
|
-
import
|
|
3
|
-
import { mkdirp, fs, system } from 'appium-support';
|
|
2
|
+
import { mkdirp, system, fs } from '@appium/support';
|
|
4
3
|
import axios from 'axios';
|
|
5
4
|
import { exec } from 'teen_process';
|
|
6
5
|
import { rootDir } from './utils';
|
|
7
6
|
import logger from './logger';
|
|
8
7
|
import semver from 'semver';
|
|
8
|
+
import findUp from 'find-up';
|
|
9
|
+
import { getDefaultsFromSchema } from './schema/schema';
|
|
9
10
|
|
|
11
|
+
const npmPackage = fs.readPackageJsonFrom(__dirname);
|
|
10
12
|
|
|
11
|
-
const npmPackage = require(path.resolve(rootDir, 'package.json'));
|
|
12
13
|
const APPIUM_VER = npmPackage.version;
|
|
13
14
|
const MIN_NODE_VERSION = npmPackage.engines.node;
|
|
14
15
|
|
|
@@ -36,11 +37,23 @@ async function updateBuildInfo (useGithubApiFallback = false) {
|
|
|
36
37
|
}
|
|
37
38
|
}
|
|
38
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Finds the Git metadata dir (see `GIT_META_ROOT`)
|
|
42
|
+
*
|
|
43
|
+
* This is needed because Appium cannot assume `package.json` and `.git` are in the same
|
|
44
|
+
* directory. Monorepos, see?
|
|
45
|
+
* @returns {string|void} Path to dir or `undefined` if not found
|
|
46
|
+
*/
|
|
47
|
+
async function findGitRoot () {
|
|
48
|
+
return await findUp(GIT_META_ROOT, {cwd: rootDir, type: 'directory'});
|
|
49
|
+
}
|
|
50
|
+
|
|
39
51
|
async function getGitRev (useGithubApiFallback = false) {
|
|
40
|
-
|
|
52
|
+
const gitRoot = await findGitRoot();
|
|
53
|
+
if (gitRoot) {
|
|
41
54
|
try {
|
|
42
55
|
const {stdout} = await exec(GIT_BINARY, ['rev-parse', 'HEAD'], {
|
|
43
|
-
cwd:
|
|
56
|
+
cwd: gitRoot
|
|
44
57
|
});
|
|
45
58
|
return stdout.trim();
|
|
46
59
|
} catch (ign) {}
|
|
@@ -68,10 +81,11 @@ async function getGitRev (useGithubApiFallback = false) {
|
|
|
68
81
|
}
|
|
69
82
|
|
|
70
83
|
async function getGitTimestamp (commitSha, useGithubApiFallback = false) {
|
|
71
|
-
|
|
84
|
+
const gitRoot = await findGitRoot();
|
|
85
|
+
if (gitRoot) {
|
|
72
86
|
try {
|
|
73
87
|
const {stdout} = await exec(GIT_BINARY, ['show', '-s', '--format=%ci', commitSha], {
|
|
74
|
-
cwd:
|
|
88
|
+
cwd: gitRoot
|
|
75
89
|
});
|
|
76
90
|
return stdout.trim();
|
|
77
91
|
} catch (ign) {}
|
|
@@ -135,65 +149,72 @@ async function showConfig () {
|
|
|
135
149
|
console.log(JSON.stringify(getBuildInfo())); // eslint-disable-line no-console
|
|
136
150
|
}
|
|
137
151
|
|
|
138
|
-
function
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
nonDefaults[arg] = args[arg];
|
|
144
|
-
}
|
|
152
|
+
function getNonDefaultServerArgs (parser, args) {
|
|
153
|
+
// hopefully these function names are descriptive enough
|
|
154
|
+
|
|
155
|
+
function typesDiffer (dest) {
|
|
156
|
+
return typeof args[dest] !== typeof defaultsFromSchema[dest];
|
|
145
157
|
}
|
|
146
|
-
return nonDefaults;
|
|
147
|
-
}
|
|
148
158
|
|
|
149
|
-
function
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
return false;
|
|
153
|
-
}
|
|
159
|
+
function defaultValueIsArray (dest) {
|
|
160
|
+
return _.isArray(defaultsFromSchema[dest]);
|
|
161
|
+
}
|
|
154
162
|
|
|
155
|
-
function
|
|
156
|
-
|
|
157
|
-
let exclusives = [
|
|
158
|
-
['noReset', 'fullReset'],
|
|
159
|
-
['ipa', 'safari'],
|
|
160
|
-
['app', 'safari'],
|
|
161
|
-
['forceIphone', 'forceIpad'],
|
|
162
|
-
['deviceName', 'defaultDevice']
|
|
163
|
-
];
|
|
164
|
-
|
|
165
|
-
for (let exSet of exclusives) {
|
|
166
|
-
let numFoundInArgs = 0;
|
|
167
|
-
for (let opt of exSet) {
|
|
168
|
-
if (_.has(args, opt) && args[opt]) {
|
|
169
|
-
numFoundInArgs++;
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
if (numFoundInArgs > 1) {
|
|
173
|
-
throw new Error(`You can't pass in more than one argument from the ` +
|
|
174
|
-
`set ${JSON.stringify(exSet)}, since they are ` +
|
|
175
|
-
`mutually exclusive`);
|
|
176
|
-
}
|
|
163
|
+
function argsValueIsArray (dest) {
|
|
164
|
+
return _.isArray(args[dest]);
|
|
177
165
|
}
|
|
178
166
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
for (let [arg, validator] of _.toPairs(validations)) {
|
|
191
|
-
if (_.has(nonDefaultArgs, arg)) {
|
|
192
|
-
if (!validator(args[arg], arg)) {
|
|
193
|
-
throw new Error(`Invalid argument for param ${arg}: ${args[arg]}`);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
167
|
+
function arraysDiffer (dest) {
|
|
168
|
+
return _.difference(args[dest], defaultsFromSchema[dest]).length > 0;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function valuesUnequal (dest) {
|
|
172
|
+
return args[dest] !== defaultsFromSchema[dest];
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function defaultIsDefined (dest) {
|
|
176
|
+
return !_.isUndefined(defaultsFromSchema[dest]);
|
|
196
177
|
}
|
|
178
|
+
|
|
179
|
+
// note that `_.overEvery` is like an "AND", and `_.overSome` is like an "OR"
|
|
180
|
+
|
|
181
|
+
const argValueNotArrayOrArraysDiffer = _.overSome([
|
|
182
|
+
_.negate(argsValueIsArray),
|
|
183
|
+
arraysDiffer
|
|
184
|
+
]);
|
|
185
|
+
|
|
186
|
+
const defaultValueNotArrayAndValuesUnequal = _.overEvery([
|
|
187
|
+
_.negate(defaultValueIsArray), valuesUnequal
|
|
188
|
+
]);
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* This used to be a hideous conditional, but it's broken up into a hideous function instead.
|
|
192
|
+
* hopefully this makes things a little more understandable.
|
|
193
|
+
* - checks if the default value is defined
|
|
194
|
+
* - if so, and the default is not an array:
|
|
195
|
+
* - ensures the types are the same
|
|
196
|
+
* - ensures the values are equal
|
|
197
|
+
* - if so, and the default is an array:
|
|
198
|
+
* - ensures the args value is an array
|
|
199
|
+
* - ensures the args values do not differ from the default values
|
|
200
|
+
* @param {string} dest - argument name (`dest` value)
|
|
201
|
+
* @returns {boolean}
|
|
202
|
+
*/
|
|
203
|
+
const isNotDefault = _.overEvery([
|
|
204
|
+
defaultIsDefined,
|
|
205
|
+
_.overSome([
|
|
206
|
+
typesDiffer,
|
|
207
|
+
_.overEvery([
|
|
208
|
+
defaultValueIsArray,
|
|
209
|
+
argValueNotArrayOrArraysDiffer
|
|
210
|
+
]),
|
|
211
|
+
defaultValueNotArrayAndValuesUnequal
|
|
212
|
+
])
|
|
213
|
+
]);
|
|
214
|
+
|
|
215
|
+
const defaultsFromSchema = getDefaultsFromSchema();
|
|
216
|
+
|
|
217
|
+
return _.pickBy(args, (__, key) => isNotDefault(key));
|
|
197
218
|
}
|
|
198
219
|
|
|
199
220
|
async function validateTmpDir (tmpDir) {
|
|
@@ -206,7 +227,7 @@ async function validateTmpDir (tmpDir) {
|
|
|
206
227
|
}
|
|
207
228
|
|
|
208
229
|
export {
|
|
209
|
-
getBuildInfo,
|
|
210
|
-
warnNodeDeprecations, validateTmpDir,
|
|
211
|
-
getGitRev,
|
|
230
|
+
getBuildInfo, checkNodeOk, showConfig,
|
|
231
|
+
warnNodeDeprecations, validateTmpDir, getNonDefaultServerArgs,
|
|
232
|
+
getGitRev, APPIUM_VER, updateBuildInfo
|
|
212
233
|
};
|
package/lib/driver-config.js
CHANGED
|
@@ -1,25 +1,74 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
1
3
|
import _ from 'lodash';
|
|
2
|
-
import ExtensionConfig
|
|
4
|
+
import ExtensionConfig from './extension-config';
|
|
5
|
+
import { DRIVER_TYPE } from './ext-config-io';
|
|
3
6
|
|
|
4
7
|
export default class DriverConfig extends ExtensionConfig {
|
|
5
|
-
|
|
8
|
+
/**
|
|
9
|
+
* A mapping of `APPIUM_HOME` values to {@link DriverConfig} instances.
|
|
10
|
+
* Each `APPIUM_HOME` should only have one associated `DriverConfig` instance.
|
|
11
|
+
* @type {Record<string,DriverConfig>}
|
|
12
|
+
* @private
|
|
13
|
+
*/
|
|
14
|
+
static _instances = {};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Call {@link DriverConfig.getInstance} instead.
|
|
18
|
+
* @private
|
|
19
|
+
* @param {string} appiumHome
|
|
20
|
+
* @param {(...args: any[]) => void} [logFn]
|
|
21
|
+
*/
|
|
22
|
+
constructor (appiumHome, logFn) {
|
|
6
23
|
super(appiumHome, DRIVER_TYPE, logFn);
|
|
24
|
+
/** @type {Set<string>} */
|
|
25
|
+
this.knownAutomationNames = new Set();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async read () {
|
|
29
|
+
this.knownAutomationNames.clear();
|
|
30
|
+
return await super.read();
|
|
7
31
|
}
|
|
8
32
|
|
|
9
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Creates or gets an instance of {@link DriverConfig} based value of `appiumHome`
|
|
35
|
+
* @param {string} appiumHome - `APPIUM_HOME` path
|
|
36
|
+
* @param {(...args: any[]) => void} [logFn] - Optional logging function
|
|
37
|
+
* @returns {DriverConfig}
|
|
38
|
+
*/
|
|
39
|
+
static getInstance (appiumHome, logFn) {
|
|
40
|
+
const instance = DriverConfig._instances[appiumHome] ?? new DriverConfig(appiumHome, logFn);
|
|
41
|
+
DriverConfig._instances[appiumHome] = instance;
|
|
42
|
+
return instance;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
*
|
|
47
|
+
* @param {object} extData
|
|
48
|
+
* @param {string} extName
|
|
49
|
+
* @returns {import('./extension-config').Problem[]}
|
|
50
|
+
*/
|
|
51
|
+
// eslint-disable-next-line no-unused-vars
|
|
52
|
+
getConfigProblems (extData, extName) {
|
|
10
53
|
const problems = [];
|
|
11
|
-
const
|
|
12
|
-
const {platformNames, automationName} = driver;
|
|
54
|
+
const {platformNames, automationName} = extData;
|
|
13
55
|
|
|
14
56
|
if (!_.isArray(platformNames)) {
|
|
15
57
|
problems.push({
|
|
16
|
-
err: 'Missing or incorrect supported
|
|
58
|
+
err: 'Missing or incorrect supported platformNames list.',
|
|
17
59
|
val: platformNames
|
|
18
60
|
});
|
|
19
61
|
} else {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
62
|
+
if (_.isEmpty(platformNames)) {
|
|
63
|
+
problems.push({
|
|
64
|
+
err: 'Empty platformNames list.',
|
|
65
|
+
val: platformNames
|
|
66
|
+
});
|
|
67
|
+
} else {
|
|
68
|
+
for (const pName of platformNames) {
|
|
69
|
+
if (!_.isString(pName)) {
|
|
70
|
+
problems.push({err: 'Incorrectly formatted platformName.', val: pName});
|
|
71
|
+
}
|
|
23
72
|
}
|
|
24
73
|
}
|
|
25
74
|
}
|
|
@@ -28,17 +77,23 @@ export default class DriverConfig extends ExtensionConfig {
|
|
|
28
77
|
problems.push({err: 'Missing or incorrect automationName', val: automationName});
|
|
29
78
|
}
|
|
30
79
|
|
|
31
|
-
if (
|
|
80
|
+
if (this.knownAutomationNames.has(automationName)) {
|
|
32
81
|
problems.push({
|
|
33
82
|
err: 'Multiple drivers claim support for the same automationName',
|
|
34
83
|
val: automationName
|
|
35
84
|
});
|
|
36
85
|
}
|
|
37
|
-
|
|
86
|
+
|
|
87
|
+
// should we retain the name at the end of this function, once we've checked there are no problems?
|
|
88
|
+
this.knownAutomationNames.add(automationName);
|
|
38
89
|
|
|
39
90
|
return problems;
|
|
40
91
|
}
|
|
41
92
|
|
|
93
|
+
/**
|
|
94
|
+
* @param {string} driverName
|
|
95
|
+
* @param {object} extData
|
|
96
|
+
*/
|
|
42
97
|
extensionDesc (driverName, {version, automationName}) {
|
|
43
98
|
return `${driverName}@${version} (automationName '${automationName}')`;
|
|
44
99
|
}
|
package/lib/drivers.js
CHANGED
|
@@ -12,9 +12,12 @@ const KNOWN_DRIVERS = {
|
|
|
12
12
|
youiengine: 'appium-youiengine-driver',
|
|
13
13
|
windows: 'appium-windows-driver',
|
|
14
14
|
mac: 'appium-mac-driver',
|
|
15
|
+
mac2: 'appium-mac2-driver',
|
|
15
16
|
espresso: 'appium-espresso-driver',
|
|
16
17
|
tizen: 'appium-tizen-driver',
|
|
17
18
|
flutter: 'appium-flutter-driver',
|
|
19
|
+
safari: 'appium-safari-driver',
|
|
20
|
+
gecko: 'appium-geckodriver',
|
|
18
21
|
};
|
|
19
22
|
|
|
20
23
|
function getDriverBySupport (drivers, matchAutomationName, matchPlatformName) {
|
|
@@ -64,7 +67,7 @@ function findMatchingDriver (config, {automationName, platformName}) {
|
|
|
64
67
|
if (!driver) {
|
|
65
68
|
throw new Error(`MainClass ${mainClass} did not result in a driver object`);
|
|
66
69
|
}
|
|
67
|
-
return {driver, version};
|
|
70
|
+
return {driver, version, driverName};
|
|
68
71
|
} catch (err) {
|
|
69
72
|
const msg = `Could not find a driver for automationName ` +
|
|
70
73
|
`'${automationName}' and platformName ${platformName}'. ` +
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Module containing {@link ExtConfigIO} which handles reading & writing of extension config files.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { fs, mkdirp } from '@appium/support';
|
|
8
|
+
import _ from 'lodash';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import YAML from 'yaml';
|
|
11
|
+
|
|
12
|
+
const CONFIG_FILE_NAME = 'extensions.yaml';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Current configuration schema revision!
|
|
16
|
+
*/
|
|
17
|
+
const CONFIG_SCHEMA_REV = 2;
|
|
18
|
+
|
|
19
|
+
export const DRIVER_TYPE = 'driver';
|
|
20
|
+
export const PLUGIN_TYPE = 'plugin';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Set of valid extension types.
|
|
24
|
+
* @type {Readonly<Set<ExtensionType>>}
|
|
25
|
+
*/
|
|
26
|
+
const VALID_EXT_TYPES = new Set([DRIVER_TYPE, PLUGIN_TYPE]);
|
|
27
|
+
|
|
28
|
+
const CONFIG_DATA_DRIVER_KEY = `${DRIVER_TYPE}s`;
|
|
29
|
+
const CONFIG_DATA_PLUGIN_KEY = `${PLUGIN_TYPE}s`;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Handles reading & writing of extension config files.
|
|
33
|
+
*
|
|
34
|
+
* Only one instance of this class exists per value of `APPIUM_HOME`.
|
|
35
|
+
*/
|
|
36
|
+
class ExtConfigIO {
|
|
37
|
+
/**
|
|
38
|
+
* "Dirty" flag. If true, the data has changed since the last write.
|
|
39
|
+
* @type {boolean}
|
|
40
|
+
* @private
|
|
41
|
+
*/
|
|
42
|
+
_dirty;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The entire contents of a parsed YAML extension config file.
|
|
46
|
+
* @type {object?}
|
|
47
|
+
* @private
|
|
48
|
+
*/
|
|
49
|
+
_data;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* A mapping of extension type to configuration data. Configuration data is
|
|
53
|
+
* keyed on extension name.
|
|
54
|
+
*
|
|
55
|
+
* Consumers get the values of this `Map` (corresponding to the
|
|
56
|
+
* `extensionType` of the consumer, which will be a subclass of
|
|
57
|
+
* `ExtensionConfig`) and do not have access to the entire data object.
|
|
58
|
+
* @private
|
|
59
|
+
* @type {Map<ExtensionType,object>}
|
|
60
|
+
*/
|
|
61
|
+
_extDataByType = new Map();
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Path to config file.
|
|
65
|
+
* @private
|
|
66
|
+
* @type {Readonly<string>}
|
|
67
|
+
*/
|
|
68
|
+
_filepath;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Path to `APPIUM_HOME`
|
|
72
|
+
* @private
|
|
73
|
+
* @type {Readonly<string>}
|
|
74
|
+
*/
|
|
75
|
+
_appiumHome;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Helps avoid writing multiple times.
|
|
79
|
+
*
|
|
80
|
+
* If this is `null`, calling {@link ExtConfigIO.write} will cause it to be
|
|
81
|
+
* set to a `Promise`. When the call to `write()` is complete, the `Promise`
|
|
82
|
+
* will resolve and then this value will be set to `null`. Concurrent calls
|
|
83
|
+
* made while this value is a `Promise` will return the `Promise` itself.
|
|
84
|
+
* @private
|
|
85
|
+
* @type {Promise<boolean>?}
|
|
86
|
+
*/
|
|
87
|
+
_writing = null;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Helps avoid reading multiple times.
|
|
91
|
+
*
|
|
92
|
+
* If this is `null`, calling {@link ExtConfigIO.read} will cause it to be
|
|
93
|
+
* set to a `Promise`. When the call to `read()` is complete, the `Promise`
|
|
94
|
+
* will resolve and then this value will be set to `null`. Concurrent calls
|
|
95
|
+
* made while this value is a `Promise` will return the `Promise` itself.
|
|
96
|
+
* @private
|
|
97
|
+
* @type {Promise<void>?}
|
|
98
|
+
*/
|
|
99
|
+
_reading = null;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* @param {string} appiumHome
|
|
103
|
+
*/
|
|
104
|
+
constructor (appiumHome) {
|
|
105
|
+
this._filepath = path.resolve(appiumHome, CONFIG_FILE_NAME);
|
|
106
|
+
this._appiumHome = appiumHome;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Creaes a `Proxy` which watches for changes to the extension-type-specific
|
|
111
|
+
* config data.
|
|
112
|
+
*
|
|
113
|
+
* When changes are detected, it sets a `_dirty` flag. The next call to
|
|
114
|
+
* {@link ExtConfigIO.write} will check if this flag is `true` before
|
|
115
|
+
* proceeding.
|
|
116
|
+
* @param {ExtensionType} extensionType
|
|
117
|
+
* @param {Record<string,object>} data - Extension config data, keyed by name
|
|
118
|
+
* @private
|
|
119
|
+
* @returns {Record<string,object>}
|
|
120
|
+
*/
|
|
121
|
+
_createProxy (extensionType, data) {
|
|
122
|
+
return new Proxy(data[`${extensionType}s`], {
|
|
123
|
+
set: (target, prop, value) => {
|
|
124
|
+
if (value !== target[prop]) {
|
|
125
|
+
this._dirty = true;
|
|
126
|
+
}
|
|
127
|
+
target[prop] = value;
|
|
128
|
+
return Reflect.set(target, prop, value);
|
|
129
|
+
},
|
|
130
|
+
deleteProperty: (target, prop) => {
|
|
131
|
+
if (prop in target) {
|
|
132
|
+
this._dirty = true;
|
|
133
|
+
}
|
|
134
|
+
return Reflect.deleteProperty(target, prop);
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Returns the path to the config file.
|
|
141
|
+
*/
|
|
142
|
+
get filepath () {
|
|
143
|
+
return this._filepath;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Gets data for an extension type. Reads the config file if necessary.
|
|
148
|
+
*
|
|
149
|
+
* Force-reading is _not_ supported, as it's likely to be a source of
|
|
150
|
+
* bugs--it's easy to mutate the data and then overwrite memory with the file
|
|
151
|
+
* contents
|
|
152
|
+
*
|
|
153
|
+
* Ideally this will only ever read the file _once_.
|
|
154
|
+
* @param {ExtensionType} extensionType - Which bit of the config data we
|
|
155
|
+
* want
|
|
156
|
+
* @returns {Promise<object>} The data
|
|
157
|
+
*/
|
|
158
|
+
async read (extensionType) {
|
|
159
|
+
if (this._reading) {
|
|
160
|
+
await this._reading;
|
|
161
|
+
return this._extDataByType.get(extensionType);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
this._reading = (async () => {
|
|
165
|
+
if (!VALID_EXT_TYPES.has(extensionType)) {
|
|
166
|
+
throw new TypeError(
|
|
167
|
+
`Invalid extension type: ${extensionType}. Valid values are: ${[
|
|
168
|
+
...VALID_EXT_TYPES,
|
|
169
|
+
].join(', ')}`,
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
if (this._extDataByType.has(extensionType)) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
let data;
|
|
177
|
+
let isNewFile = false;
|
|
178
|
+
try {
|
|
179
|
+
await mkdirp(this._appiumHome);
|
|
180
|
+
const yaml = await fs.readFile(this.filepath, 'utf8');
|
|
181
|
+
data = YAML.parse(yaml);
|
|
182
|
+
} catch (err) {
|
|
183
|
+
if (err.code === 'ENOENT') {
|
|
184
|
+
data = {
|
|
185
|
+
[CONFIG_DATA_DRIVER_KEY]: {},
|
|
186
|
+
[CONFIG_DATA_PLUGIN_KEY]: {},
|
|
187
|
+
schemaRev: CONFIG_SCHEMA_REV,
|
|
188
|
+
};
|
|
189
|
+
isNewFile = true;
|
|
190
|
+
} else {
|
|
191
|
+
throw new Error(
|
|
192
|
+
`Appium had trouble loading the extension installation ` +
|
|
193
|
+
`cache file (${this.filepath}). Ensure it exists and is ` +
|
|
194
|
+
`readable. Specific error: ${err.message}`,
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
this._data = data;
|
|
200
|
+
this._extDataByType.set(
|
|
201
|
+
DRIVER_TYPE,
|
|
202
|
+
this._createProxy(DRIVER_TYPE, data),
|
|
203
|
+
);
|
|
204
|
+
this._extDataByType.set(
|
|
205
|
+
PLUGIN_TYPE,
|
|
206
|
+
this._createProxy(PLUGIN_TYPE, data),
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
if (isNewFile) {
|
|
210
|
+
await this.write(true);
|
|
211
|
+
}
|
|
212
|
+
})();
|
|
213
|
+
try {
|
|
214
|
+
await this._reading;
|
|
215
|
+
return this._extDataByType.get(extensionType);
|
|
216
|
+
} finally {
|
|
217
|
+
this._reading = null;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Writes the data if it needs writing.
|
|
223
|
+
*
|
|
224
|
+
* If the `schemaRev` prop needs updating, the file will be written.
|
|
225
|
+
* @param {boolean} [force=false] - Whether to force a write even if the data is clean
|
|
226
|
+
* @returns {Promise<boolean>} Whether the data was written
|
|
227
|
+
*/
|
|
228
|
+
async write (force = false) {
|
|
229
|
+
if (this._writing) {
|
|
230
|
+
return this._writing;
|
|
231
|
+
}
|
|
232
|
+
this._writing = (async () => {
|
|
233
|
+
try {
|
|
234
|
+
if (!this._dirty && !force) {
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (!this._data) {
|
|
239
|
+
throw new ReferenceError('No data to write. Call `read()` first');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const dataToWrite = {
|
|
243
|
+
...this._data,
|
|
244
|
+
[CONFIG_DATA_DRIVER_KEY]: this._extDataByType.get(DRIVER_TYPE),
|
|
245
|
+
[CONFIG_DATA_PLUGIN_KEY]: this._extDataByType.get(PLUGIN_TYPE),
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
try {
|
|
249
|
+
await fs.writeFile(
|
|
250
|
+
this.filepath,
|
|
251
|
+
YAML.stringify(dataToWrite),
|
|
252
|
+
'utf8',
|
|
253
|
+
);
|
|
254
|
+
this._dirty = false;
|
|
255
|
+
return true;
|
|
256
|
+
} catch {
|
|
257
|
+
throw new Error(
|
|
258
|
+
`Appium could not parse or write from the Appium Home directory ` +
|
|
259
|
+
`(${this._appiumHome}). Please ensure it is writable.`,
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
} finally {
|
|
263
|
+
this._writing = null;
|
|
264
|
+
}
|
|
265
|
+
})();
|
|
266
|
+
return await this._writing;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Factory function for {@link ExtConfigIO}.
|
|
272
|
+
*
|
|
273
|
+
* Maintains one instance per value of `APPIUM_HOME`.
|
|
274
|
+
* @param {string} appiumHome - `APPIUM_HOME`
|
|
275
|
+
* @returns {ExtConfigIO}
|
|
276
|
+
*/
|
|
277
|
+
export const getExtConfigIOInstance = _.memoize(
|
|
278
|
+
(appiumHome) => new ExtConfigIO(appiumHome),
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* @typedef {ExtConfigIO} ExtensionConfigIO
|
|
283
|
+
*/
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* @typedef {typeof DRIVER_TYPE | typeof PLUGIN_TYPE} ExtensionType
|
|
287
|
+
*/
|