@testim/testim-cli 3.212.0 → 3.216.0
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/agent/routers/cliJsCode/service.js +46 -85
- package/agent/routers/codim/service.js +0 -2
- package/cliAgentMode.js +0 -1
- package/codim/template.js/package.json +2 -2
- package/codim/template.ts/package.json +2 -2
- package/commons/featureFlags.js +1 -1
- package/commons/httpRequest.js +21 -9
- package/commons/httpRequestCounters.js +41 -10
- package/commons/httpRequestCounters.test.js +9 -9
- package/commons/npmWrapper.js +14 -7
- package/commons/npmWrapper.test.js +1 -1
- package/commons/testimDesiredCapabilitiesBuilder.js +21 -0
- package/npm-shrinkwrap.json +21848 -6389
- package/package.json +7 -8
- package/player/seleniumTestPlayer.js +4 -2
- package/player/services/frameLocator.js +106 -102
- package/player/services/tabService.js +28 -28
- package/player/stepActions/inputFileStepAction.js +4 -1
- package/player/stepActions/pixelValidationStepAction.js +8 -1
- package/player/utils/imageCaptureUtils.js +10 -1
- package/player/utils/screenshotUtils.js +1 -1
- package/runOptions.js +22 -4
- package/runner.js +4 -3
- package/services/lambdatestService.js +1 -0
- package/testRunHandler.js +16 -5
- package/workers/BaseWorker.js +10 -10
- package/workers/WorkerExtension.js +6 -6
- package/workers/WorkerSelenium.js +5 -10
- package/workers/workerUtils.js +0 -36
- package/agent/routers/cliJsCode/runNpmWorker.js +0 -39
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const findRoot = require('find-root');
|
|
5
5
|
const dataUriToBuffer = require('data-uri-to-buffer');
|
|
6
|
-
const {spawn: threadSpawn, config} = require('threads');
|
|
6
|
+
const { spawn: threadSpawn, config } = require('threads');
|
|
7
7
|
const Promise = require('bluebird');
|
|
8
8
|
const fs = require('fs-extra');
|
|
9
9
|
const utils = require('../../../utils');
|
|
@@ -11,7 +11,7 @@ const logger = require('../../../commons/logger').getLogger("cli-service");
|
|
|
11
11
|
const { getS3Artifact } = require('../../../commons/testimServicesApi');
|
|
12
12
|
const npmWrapper = require('../../../commons/npmWrapper');
|
|
13
13
|
const featureFlags = require('../../../commons/featureFlags');
|
|
14
|
-
const {NpmPackageError} = require('../../../errors');
|
|
14
|
+
const { NpmPackageError } = require('../../../errors');
|
|
15
15
|
|
|
16
16
|
config.set({
|
|
17
17
|
basepath: {
|
|
@@ -210,10 +210,10 @@ function runCode(transactionId, incomingParams, context, code, packageLocalLocat
|
|
|
210
210
|
const thread = threadSpawn(constructWithArguments(Function, ['input', 'done', 'progress', runFn]));
|
|
211
211
|
return new Promise((resolve) => {
|
|
212
212
|
thread
|
|
213
|
-
.send({incomingParams, context, code})
|
|
213
|
+
.send({ incomingParams, context, code })
|
|
214
214
|
.on('message', message => {
|
|
215
|
-
const messageWithLogs = Object.assign({}, message, {tstConsoleLogs: testimConsoleLogDataAggregates})
|
|
216
|
-
logger.debug('Run code worker response', {messageWithLogs, transactionId});
|
|
215
|
+
const messageWithLogs = Object.assign({}, message, { tstConsoleLogs: testimConsoleLogDataAggregates })
|
|
216
|
+
logger.debug('Run code worker response', { messageWithLogs, transactionId });
|
|
217
217
|
resolve(messageWithLogs);
|
|
218
218
|
})
|
|
219
219
|
.on('progress', (logMessage) => {
|
|
@@ -221,11 +221,11 @@ function runCode(transactionId, incomingParams, context, code, packageLocalLocat
|
|
|
221
221
|
})
|
|
222
222
|
.on('error', (err) => {
|
|
223
223
|
if (err.message === "malformed data: URI") {
|
|
224
|
-
logger.error('Run code worker error', {err, transactionId, fileDataUrl});
|
|
224
|
+
logger.error('Run code worker error', { err, transactionId, fileDataUrl });
|
|
225
225
|
} else {
|
|
226
|
-
logger.error('Run code worker error', {err, transactionId});
|
|
226
|
+
logger.error('Run code worker error', { err, transactionId });
|
|
227
227
|
}
|
|
228
|
-
|
|
228
|
+
|
|
229
229
|
resolve({
|
|
230
230
|
tstConsoleLogs: testimConsoleLogDataAggregates,
|
|
231
231
|
status: 'failed',
|
|
@@ -239,11 +239,11 @@ function runCode(transactionId, incomingParams, context, code, packageLocalLocat
|
|
|
239
239
|
});
|
|
240
240
|
})
|
|
241
241
|
.on('exit', () => {
|
|
242
|
-
logger.debug('Run code worker has been terminated', {transactionId});
|
|
242
|
+
logger.debug('Run code worker has been terminated', { transactionId });
|
|
243
243
|
});
|
|
244
244
|
}).timeout(timeout)
|
|
245
245
|
.catch(Promise.TimeoutError, err => {
|
|
246
|
-
logger.warn("timeout to run code", {transactionId, err});
|
|
246
|
+
logger.warn("timeout to run code", { transactionId, err });
|
|
247
247
|
return Promise.resolve({
|
|
248
248
|
tstConsoleLogs: testimConsoleLogDataAggregates,
|
|
249
249
|
status: 'failed',
|
|
@@ -254,7 +254,7 @@ function runCode(transactionId, incomingParams, context, code, packageLocalLocat
|
|
|
254
254
|
exportsGlobal: {}
|
|
255
255
|
},
|
|
256
256
|
success: false
|
|
257
|
-
});
|
|
257
|
+
});
|
|
258
258
|
})
|
|
259
259
|
.finally(() => thread && thread.kill());
|
|
260
260
|
}
|
|
@@ -264,7 +264,7 @@ function removeFolder(installFolder) {
|
|
|
264
264
|
return fs.remove(installFolder)
|
|
265
265
|
.then(resolve)
|
|
266
266
|
.catch(err => {
|
|
267
|
-
logger.warn(`failed to remove install npm packages folder`, {err});
|
|
267
|
+
logger.warn(`failed to remove install npm packages folder`, { err });
|
|
268
268
|
return resolve();
|
|
269
269
|
});
|
|
270
270
|
});
|
|
@@ -272,7 +272,7 @@ function removeFolder(installFolder) {
|
|
|
272
272
|
|
|
273
273
|
function cleanPackages(transactionId) {
|
|
274
274
|
function cleanInstallFolder() {
|
|
275
|
-
const {installFolder} = transactions[transactionId];
|
|
275
|
+
const { installFolder } = transactions[transactionId];
|
|
276
276
|
if (!installFolder) {
|
|
277
277
|
return Promise.resolve();
|
|
278
278
|
}
|
|
@@ -310,16 +310,10 @@ function mapNpmInstallDataToInstallData(npmInstallData, packageData) {
|
|
|
310
310
|
function installPackage(stepId, testResultId, retryIndex, packageData, stepResultId, timeout) {
|
|
311
311
|
const transactionId = getTransactionId(stepResultId, testResultId, stepId, retryIndex);
|
|
312
312
|
return runNpmInstall(transactionId, packageData, timeout)
|
|
313
|
-
.then(({data, installFolder}) => {
|
|
313
|
+
.then(({ data, installFolder }) => {
|
|
314
314
|
transactions[transactionId] = transactions[transactionId] || {};
|
|
315
315
|
transactions[transactionId].installFolder = installFolder;
|
|
316
316
|
return Promise.resolve(data);
|
|
317
|
-
}).then(npmInstallData => {
|
|
318
|
-
if (!featureFlags.flags.enableNpmPackageInstallUsingNpmCli.isEnabled()) {
|
|
319
|
-
return mapNpmInstallDataToInstallData(npmInstallData, packageData)
|
|
320
|
-
} else {
|
|
321
|
-
return npmInstallData;
|
|
322
|
-
}
|
|
323
317
|
});
|
|
324
318
|
|
|
325
319
|
}
|
|
@@ -339,80 +333,47 @@ function runCodeWithPackages(code, stepId, incomingParams, context, testResultId
|
|
|
339
333
|
if (s3fileDataUrl) {
|
|
340
334
|
fileDataUrl = s3fileDataUrl;
|
|
341
335
|
}
|
|
342
|
-
})
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
.finally(() => cleanPackages(transactionId));
|
|
336
|
+
}).then(() => runCode(transactionId, incomingParams, context, code, packageLocalLocations, timeout, fileDataUrl))
|
|
337
|
+
.then(res => Object.assign({}, res, { nodeVersion: process.version }))
|
|
338
|
+
.finally(() => cleanPackages(transactionId));
|
|
346
339
|
}
|
|
347
340
|
|
|
348
341
|
function runNpmInstall(transactionId, packageData, timeout) {
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
const packageLines = output.match(/\+ (\w|-)+@(\d|.)+/g);
|
|
363
|
-
const fullnames = packageLines.map(l => l.substr(2));
|
|
364
|
-
|
|
365
|
-
const packageDataWithVersions = packageData.map(pData => {
|
|
366
|
-
const packageFullName = fullnames.find(name => {
|
|
367
|
-
const strudelIndex = name.lastIndexOf('@');
|
|
368
|
-
const npmInstalledPackageName = name.substr(0, strudelIndex);
|
|
369
|
-
return npmInstalledPackageName === pData.packageName;
|
|
370
|
-
});
|
|
371
|
-
const packageLocalLocation = path.resolve(installFolder, 'node_modules', pData.packageName);
|
|
372
|
-
const packageWithVersion = Object.assign(pData, { packageFullName, packageLocalLocation});
|
|
373
|
-
return packageWithVersion;
|
|
374
|
-
});
|
|
375
|
-
|
|
376
|
-
resolve({data:packageDataWithVersions, installFolder});
|
|
377
|
-
} catch (err) {
|
|
378
|
-
logger.warn('npm package install failed', {transactionId, err});
|
|
379
|
-
reject(err);
|
|
342
|
+
const packages = packageData.map(data => `${data.packageName}@${data.packageVersion}`);
|
|
343
|
+
const localPackageInstallFolder = getLocalPackageInstallFolder();
|
|
344
|
+
const installFolder = path.join(localPackageInstallFolder, `/${transactionId}`);
|
|
345
|
+
const proxyUri = global.proxyUri;
|
|
346
|
+
return new Promise(async (resolve, reject) => {
|
|
347
|
+
let output = '';
|
|
348
|
+
try {
|
|
349
|
+
output = await npmWrapper.installPackages(installFolder, packages, proxyUri, timeout);
|
|
350
|
+
logger.info('npm package install finished', { transactionId, output, timeout });
|
|
351
|
+
if (Number(output.trim().split(' ')[1]) < packages.length) {
|
|
352
|
+
reject('npm package install failed, couldn\'t install all packages');
|
|
353
|
+
return;
|
|
380
354
|
}
|
|
355
|
+
const packageDataWithVersions = packageData.map(pData => {
|
|
356
|
+
const version = npmWrapper.getLocallyInstalledPackageVersion(installFolder, pData.packageName);
|
|
357
|
+
const packageFullName = `${pData.packageName}@${version}`;
|
|
358
|
+
const packageLocalLocation = path.resolve(installFolder, 'node_modules', pData.packageName);
|
|
359
|
+
return Object.assign({}, pData, {
|
|
360
|
+
packageFullName,
|
|
361
|
+
packageLocalLocation
|
|
362
|
+
})
|
|
363
|
+
});
|
|
381
364
|
|
|
382
|
-
|
|
365
|
+
resolve({ data: packageDataWithVersions, installFolder });
|
|
366
|
+
} catch (err) {
|
|
367
|
+
logger.warn('npm package install failed', { transactionId, err });
|
|
368
|
+
reject(err);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
})
|
|
383
372
|
.timeout(timeout)
|
|
384
373
|
.catch(Promise.TimeoutError, err => {
|
|
385
|
-
logger.warn("timeout to install package", {packages, transactionId, err, timeout});
|
|
374
|
+
logger.warn("timeout to install package", { packages, transactionId, err, timeout });
|
|
386
375
|
throw err;
|
|
387
376
|
});
|
|
388
|
-
} else {
|
|
389
|
-
const thread = threadSpawn('runNpmWorker.js');
|
|
390
|
-
const localPackageInstallFolder = getLocalPackageInstallFolder();
|
|
391
|
-
const packages = packageData.map(data => `${data.packageName}@${data.packageVersion}`);
|
|
392
|
-
return new Promise((resolve, reject) => {
|
|
393
|
-
thread
|
|
394
|
-
.send({transactionId, packages, command: "install", localPackageInstallFolder, proxyUri: global.proxyUri})
|
|
395
|
-
.on('message', message => {
|
|
396
|
-
resolve(message);
|
|
397
|
-
logger.debug("Npm worker response", {message, transactionId});
|
|
398
|
-
})
|
|
399
|
-
.on('error', err => {
|
|
400
|
-
if (err.stack.includes("NpmPackageError")) {
|
|
401
|
-
return reject(new NpmPackageError(err.message));
|
|
402
|
-
}
|
|
403
|
-
return reject(err);
|
|
404
|
-
});
|
|
405
|
-
})
|
|
406
|
-
.timeout(timeout)
|
|
407
|
-
.catch(Promise.TimeoutError, err => {
|
|
408
|
-
logger.warn("timeout to install package", {packageData, transactionId, err});
|
|
409
|
-
throw err;
|
|
410
|
-
})
|
|
411
|
-
.finally(() => thread && thread.kill());
|
|
412
|
-
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
|
|
416
377
|
}
|
|
417
378
|
|
|
418
379
|
function getLocalPackageInstallFolder() {
|
|
@@ -107,7 +107,6 @@ async function saveTest({
|
|
|
107
107
|
await fs.writeFileAsync(filename, body);
|
|
108
108
|
await fs.mkdirAsync(path.join(folder, 'locators')).catch(() => {});
|
|
109
109
|
for (const { id, body } of locators) {
|
|
110
|
-
// eslint-disable-next-line no-await-in-loop
|
|
111
110
|
await fs.writeFileAsync(path.join(folder, 'locators', `locator.${id}.json`), JSON.stringify(body));
|
|
112
111
|
}
|
|
113
112
|
const locatorMap = fromPairs(locators.map(({ name, id }) => [name, id]));
|
|
@@ -129,7 +128,6 @@ async function saveLocators(locators, { mergeIntoExisting } = { mergeIntoExistin
|
|
|
129
128
|
await fs.mkdirAsync(path.join(folder, 'locators')).catch(() => {});
|
|
130
129
|
|
|
131
130
|
for (const { name, id, elementLocator } of locators) {
|
|
132
|
-
// eslint-disable-next-line no-await-in-loop
|
|
133
131
|
await fs.writeFileAsync(path.join(folder, 'locators', `locator.${id}.json`), JSON.stringify({ name, id, elementLocator }));
|
|
134
132
|
}
|
|
135
133
|
const locatorMap = fromPairs(locators.map(({ name, id }) => [name, id]));
|
package/cliAgentMode.js
CHANGED
package/commons/featureFlags.js
CHANGED
|
@@ -38,7 +38,6 @@ class FeatureFlagsService {
|
|
|
38
38
|
useClickimVisibilityChecks: new Rox.Flag(),
|
|
39
39
|
useIEWebdriverVisibilityChecks: new Rox.Flag(),
|
|
40
40
|
runGetElementCodeInAut: new Rox.Flag(),
|
|
41
|
-
enableNpmPackageInstallUsingNpmCli: new Rox.Flag(),
|
|
42
41
|
enableFrameSwitchOptimization: new Rox.Flag(),
|
|
43
42
|
maximumJsResultSize: new Rox.Configuration(2000 * 1024),
|
|
44
43
|
skipFileInputClicks: new Rox.Flag(),
|
|
@@ -56,6 +55,7 @@ class FeatureFlagsService {
|
|
|
56
55
|
usePortedHtml5DragDrop: new Rox.Flag(),
|
|
57
56
|
applitoolsNewIntegration: new Rox.Flag(),
|
|
58
57
|
testNamesToBeforeSuiteHook: new Rox.Flag(),
|
|
58
|
+
addCustomCapabilities: new Rox.Variant('{}'),
|
|
59
59
|
};
|
|
60
60
|
Rox.register('default', this.flags);
|
|
61
61
|
}
|
package/commons/httpRequest.js
CHANGED
|
@@ -32,6 +32,11 @@ function getProxy() {
|
|
|
32
32
|
return global.proxyUri;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
const logErrorAndRethrow = (logMsg, data) => err => {
|
|
36
|
+
logger.error(logMsg, { ...data, error: err });
|
|
37
|
+
throw err;
|
|
38
|
+
};
|
|
39
|
+
|
|
35
40
|
function deleteMethod(url, headers, timeout) {
|
|
36
41
|
return deleteFullRes(url, headers, timeout)
|
|
37
42
|
.then(res => {
|
|
@@ -40,7 +45,7 @@ function deleteMethod(url, headers, timeout) {
|
|
|
40
45
|
}
|
|
41
46
|
return res.body;
|
|
42
47
|
})
|
|
43
|
-
.
|
|
48
|
+
.catch(logErrorAndRethrow('failed to delete request', { url }));
|
|
44
49
|
}
|
|
45
50
|
|
|
46
51
|
function deleteFullRes(url, headers = {}, timeout = DEFAULT_REQUEST_TIMEOUT) {
|
|
@@ -70,7 +75,7 @@ function post({
|
|
|
70
75
|
}
|
|
71
76
|
return res.body;
|
|
72
77
|
})
|
|
73
|
-
.
|
|
78
|
+
.catch(logErrorAndRethrow('failed to post request', { url }));
|
|
74
79
|
}
|
|
75
80
|
|
|
76
81
|
function postFullRes(url, body, headers = {}, timeout = DEFAULT_REQUEST_TIMEOUT, retry) {
|
|
@@ -128,7 +133,7 @@ function postForm(url, fields, files, headers = {}, timeout = DEFAULT_REQUEST_TI
|
|
|
128
133
|
}
|
|
129
134
|
return res.body;
|
|
130
135
|
})
|
|
131
|
-
.
|
|
136
|
+
.catch(logErrorAndRethrow('failed to post request', { url }));
|
|
132
137
|
}
|
|
133
138
|
|
|
134
139
|
function _get(url, query, headers = {}, timeout = DEFAULT_REQUEST_TIMEOUT, { isBinary = false, skipProxy = false } = {}) {
|
|
@@ -156,13 +161,16 @@ function _get(url, query, headers = {}, timeout = DEFAULT_REQUEST_TIMEOUT, { isB
|
|
|
156
161
|
function getText(url, query, headers) {
|
|
157
162
|
return _get(url, query, headers)
|
|
158
163
|
.then((res) => res.text)
|
|
159
|
-
.
|
|
164
|
+
.catch(logErrorAndRethrow('failed to getText request', { url, query }));
|
|
160
165
|
}
|
|
161
166
|
|
|
162
167
|
function get(url, query, headers, timeout, options) {
|
|
163
168
|
return _get(url, query, headers, timeout, options)
|
|
164
169
|
.then((res) => res.body)
|
|
165
|
-
.
|
|
170
|
+
.catch(err => {
|
|
171
|
+
logger.warn('failed to get request', { url, query, error: err });
|
|
172
|
+
throw err;
|
|
173
|
+
});
|
|
166
174
|
}
|
|
167
175
|
|
|
168
176
|
function getFullRes(url, query, headers, timeout) {
|
|
@@ -183,7 +191,7 @@ function head(url) {
|
|
|
183
191
|
}
|
|
184
192
|
|
|
185
193
|
return Promise.fromCallback((callback) => request.end(callback))
|
|
186
|
-
.
|
|
194
|
+
.catch(logErrorAndRethrow('failed to head request', { url }));
|
|
187
195
|
}
|
|
188
196
|
|
|
189
197
|
function put(url, body, headers = {}, timeout = DEFAULT_REQUEST_TIMEOUT) {
|
|
@@ -203,7 +211,7 @@ function put(url, body, headers = {}, timeout = DEFAULT_REQUEST_TIMEOUT) {
|
|
|
203
211
|
|
|
204
212
|
return Promise.fromCallback((callback) => request.end(callback))
|
|
205
213
|
.then((res) => res.body)
|
|
206
|
-
.
|
|
214
|
+
.catch(logErrorAndRethrow('failed to put request', { url }));
|
|
207
215
|
}
|
|
208
216
|
|
|
209
217
|
function download(url) {
|
|
@@ -224,8 +232,11 @@ function download(url) {
|
|
|
224
232
|
}
|
|
225
233
|
|
|
226
234
|
return Promise.fromCallback((callback) => request.end(callback))
|
|
227
|
-
.
|
|
228
|
-
|
|
235
|
+
.then(response => {
|
|
236
|
+
logger.info('finished to download', { url });
|
|
237
|
+
return response;
|
|
238
|
+
})
|
|
239
|
+
.catch(logErrorAndRethrow('failed to download', { url }));
|
|
229
240
|
}
|
|
230
241
|
|
|
231
242
|
module.exports = {
|
|
@@ -241,4 +252,5 @@ module.exports = {
|
|
|
241
252
|
head: wrapWithMonitoring(head),
|
|
242
253
|
download: wrapWithMonitoring(download),
|
|
243
254
|
isNetworkHealthy: wrapWithMonitoring.isNetworkHealthy,
|
|
255
|
+
didNetworkConnectivityTestFail: wrapWithMonitoring.didNetworkConnectivityTestFail,
|
|
244
256
|
};
|
|
@@ -1,7 +1,36 @@
|
|
|
1
1
|
'use strit';
|
|
2
2
|
|
|
3
3
|
const { sum } = require('lodash');
|
|
4
|
-
const
|
|
4
|
+
const Bluebird = require('bluebird');
|
|
5
|
+
const dns = require('dns');
|
|
6
|
+
const _ = require('lodash');
|
|
7
|
+
const config = require('./config');
|
|
8
|
+
|
|
9
|
+
const logger = require('./logger').getLogger('http-request-counters');
|
|
10
|
+
|
|
11
|
+
let networkConnectivityTestFailed = false;
|
|
12
|
+
|
|
13
|
+
/** Tests network connectivity with DNS resolution (a basic test for a 7 later stack parallel but essential to most HTTP requests) */
|
|
14
|
+
const testNetworkConnectivity = async () => {
|
|
15
|
+
if (config.IS_ON_PREM) {
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
const hostnames = ['www.google.com', 'www.facebook.com', 'www.microsoft.com', 'testim.io'];
|
|
19
|
+
try {
|
|
20
|
+
// If any of these domains resolve we consider the connectivity to be ok
|
|
21
|
+
const result = Boolean(await Bluebird.any(hostnames.map(host => dns.promises.lookup(host))));
|
|
22
|
+
if (!result) {
|
|
23
|
+
networkConnectivityTestFailed = true;
|
|
24
|
+
}
|
|
25
|
+
return result;
|
|
26
|
+
} catch (e) {
|
|
27
|
+
logger.error('network connectivity test failed');
|
|
28
|
+
networkConnectivityTestFailed = true;
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
const throttledTestNetworkConnectivity = _.throttle(testNetworkConnectivity, 10 * 1000);
|
|
33
|
+
|
|
5
34
|
// we remove entries after 15 minutes, note that this isn't accurate because
|
|
6
35
|
// we remove the "fail"/"success" 10 minutes after we add them (and not the "call")
|
|
7
36
|
// this is fine since these values are an estimation and not an accurate representation
|
|
@@ -24,7 +53,7 @@ module.exports.makeCounters = () => {
|
|
|
24
53
|
}, ttl);
|
|
25
54
|
}
|
|
26
55
|
function wrapWithMonitoring(fn, name = fn.name) {
|
|
27
|
-
return method(async function (...args) {
|
|
56
|
+
return Bluebird.method(async function (...args) {
|
|
28
57
|
update(counters.call, name);
|
|
29
58
|
try {
|
|
30
59
|
const result = await fn.call(this, ...args);
|
|
@@ -32,24 +61,26 @@ module.exports.makeCounters = () => {
|
|
|
32
61
|
return result;
|
|
33
62
|
} catch (e) {
|
|
34
63
|
update(counters.fail, name);
|
|
64
|
+
if (!networkConnectivityTestFailed) {
|
|
65
|
+
throttledTestNetworkConnectivity();
|
|
66
|
+
}
|
|
35
67
|
throw e;
|
|
36
68
|
}
|
|
37
69
|
});
|
|
38
70
|
}
|
|
39
|
-
wrapWithMonitoring.isNetworkHealthy = function isNetworkHealthy() {
|
|
71
|
+
wrapWithMonitoring.isNetworkHealthy = async function isNetworkHealthy() {
|
|
72
|
+
if (networkConnectivityTestFailed || !(await testNetworkConnectivity())) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
40
75
|
const allFailed = sum([...counters.fail.values()]);
|
|
41
|
-
const allSuccess = sum([...counters.success.values()]);
|
|
42
76
|
const allCalls = sum([...counters.call.values()]);
|
|
43
77
|
// we declare a test unhealthy network wise if
|
|
44
|
-
//
|
|
45
|
-
// 2. more than 10% of requests (out of finished requests) failed
|
|
78
|
+
// 10% or more of requests (out of finished requests) failed
|
|
46
79
|
// note that the network can be unhealthy but the test would still pass
|
|
47
|
-
|
|
48
|
-
return allFailed < 10;
|
|
49
|
-
}
|
|
50
|
-
return (allSuccess + allFailed) > allFailed * 10;
|
|
80
|
+
return allFailed < allCalls * 0.1;
|
|
51
81
|
};
|
|
52
82
|
wrapWithMonitoring.counters = counters; // expose the counters used to the outside
|
|
53
83
|
wrapWithMonitoring.isNetworkHealthy.counters = wrapWithMonitoring.counters;
|
|
84
|
+
wrapWithMonitoring.didNetworkConnectivityTestFail = () => networkConnectivityTestFailed;
|
|
54
85
|
return wrapWithMonitoring;
|
|
55
86
|
};
|
|
@@ -9,30 +9,30 @@ describe('the http request counters', () => {
|
|
|
9
9
|
});
|
|
10
10
|
|
|
11
11
|
it('Marks an always failing network as unhealthy', async () => {
|
|
12
|
-
const fn = runWithRetries(wrapWithMonitoring(() => { throw new Error('bad network'); }),
|
|
12
|
+
const fn = runWithRetries(wrapWithMonitoring(() => { throw new Error('bad network'); }), 30, 1);
|
|
13
13
|
await rejects(fn);
|
|
14
|
-
strictEqual(wrapWithMonitoring.isNetworkHealthy(), false);
|
|
14
|
+
strictEqual(await wrapWithMonitoring.isNetworkHealthy(), false);
|
|
15
15
|
});
|
|
16
16
|
|
|
17
17
|
it('Marks an unstable network as unhealthy', async () => {
|
|
18
|
-
const fn = runWithRetries(wrapWithMonitoring(() => { throw new Error('bad network'); }),
|
|
18
|
+
const fn = runWithRetries(wrapWithMonitoring(() => { throw new Error('bad network'); }), 30, 1);
|
|
19
19
|
const fn2 = runWithRetries(wrapWithMonitoring(() => 'hello'), 20, 1);
|
|
20
20
|
await rejects(fn);
|
|
21
21
|
await doesNotReject(fn2);
|
|
22
|
-
strictEqual(wrapWithMonitoring.isNetworkHealthy(), false);
|
|
22
|
+
strictEqual(await wrapWithMonitoring.isNetworkHealthy(), false);
|
|
23
23
|
});
|
|
24
24
|
|
|
25
|
-
it('Marks a
|
|
26
|
-
const fn = runWithRetries(wrapWithMonitoring(() => { throw new Error('bad network'); }),
|
|
25
|
+
it('Marks a trivial amount of failed requests as healthy', async () => {
|
|
26
|
+
const fn = runWithRetries(wrapWithMonitoring(() => { throw new Error('bad network'); }), 30, 1);
|
|
27
27
|
await rejects(fn);
|
|
28
28
|
const fn2 = wrapWithMonitoring(() => 'hello');
|
|
29
|
-
await Promise.all(Array(
|
|
30
|
-
strictEqual(wrapWithMonitoring.isNetworkHealthy(), true);
|
|
29
|
+
await Promise.all(Array(290).fill().map(fn2));
|
|
30
|
+
strictEqual(await wrapWithMonitoring.isNetworkHealthy(), true);
|
|
31
31
|
});
|
|
32
32
|
|
|
33
33
|
it('Marks a healthy network as healthy', async () => {
|
|
34
34
|
const fn2 = wrapWithMonitoring(() => 'hello');
|
|
35
35
|
await Promise.all(Array(200).fill().map(fn2));
|
|
36
|
-
strictEqual(wrapWithMonitoring.isNetworkHealthy(), true);
|
|
36
|
+
strictEqual(await wrapWithMonitoring.isNetworkHealthy(), true);
|
|
37
37
|
});
|
|
38
38
|
});
|
package/commons/npmWrapper.js
CHANGED
|
@@ -60,6 +60,11 @@ async function installPackageLocally(rootPath, packageName, execOptions) {
|
|
|
60
60
|
return regexResult[1];
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
// https://github.com/npm/arborist/pull/362
|
|
64
|
+
function npmSevenAndEightMissingPermissions(error) {
|
|
65
|
+
return /The "to" argument must be of type string./.exec(error.stderr);
|
|
66
|
+
}
|
|
67
|
+
|
|
63
68
|
// this is here because our shrinkwrap blocks our lazy deps for some reason
|
|
64
69
|
const oldShrinkwrap = path.join(rootPath, 'npm-shrinkwrap.json');
|
|
65
70
|
const newShrinkwrap = path.join(rootPath, 'npm-shrinkwrap-dummy.json');
|
|
@@ -73,23 +78,24 @@ async function installPackageLocally(rootPath, packageName, execOptions) {
|
|
|
73
78
|
} catch (err) {
|
|
74
79
|
// ignore error
|
|
75
80
|
}
|
|
76
|
-
return await exec(`npm i ${packageName} --no-save --no-package-lock --no-prune --prefer-offline --no-audit --progress=false`, { ...execOptions, cwd: rootPath }).catch(err => {
|
|
81
|
+
return await exec(`npm i ${packageName} --no-save --no-package-lock --no-prune --prefer-offline --no-audit --legacy-peer-deps --progress=false`, { ...execOptions, cwd: rootPath }).catch(err => {
|
|
77
82
|
const pathWithMissingPermissions = getPathWithMissingPermissions(err);
|
|
78
|
-
|
|
83
|
+
const npmEightMissingPermissions = npmSevenAndEightMissingPermissions(err);
|
|
84
|
+
if (pathWithMissingPermissions || npmEightMissingPermissions) {
|
|
79
85
|
logger.info('Failed to install package due to insufficient write access', {
|
|
80
86
|
...additionalLogDetails(),
|
|
81
87
|
package: packageName,
|
|
82
|
-
path: pathWithMissingPermissions,
|
|
88
|
+
path: pathWithMissingPermissions || rootPath,
|
|
83
89
|
});
|
|
84
90
|
// eslint-disable-next-line no-console
|
|
85
91
|
console.error(`
|
|
86
92
|
|
|
87
93
|
Testim failed installing the package ${packageName} due to insufficient permissions.
|
|
88
94
|
This is probably due to an installation of @testim/testim-cli with sudo, and running it without sudo.
|
|
89
|
-
Testim had missing write access to ${pathWithMissingPermissions}
|
|
95
|
+
Testim had missing write access to ${pathWithMissingPermissions || rootPath}
|
|
90
96
|
|
|
91
97
|
`);
|
|
92
|
-
throw new NpmPermissionsError(pathWithMissingPermissions);
|
|
98
|
+
throw new NpmPermissionsError(pathWithMissingPermissions || rootPath);
|
|
93
99
|
}
|
|
94
100
|
throw err;
|
|
95
101
|
});
|
|
@@ -104,7 +110,7 @@ Testim had missing write access to ${pathWithMissingPermissions}
|
|
|
104
110
|
}
|
|
105
111
|
}
|
|
106
112
|
|
|
107
|
-
const localNpmLocation = path.resolve(require.resolve('npm'), '
|
|
113
|
+
const localNpmLocation = path.resolve(require.resolve('npm').replace('index.js',''), 'bin','npm-cli.js');
|
|
108
114
|
|
|
109
115
|
function installPackages(prefix, packageNames, proxyUri, timeoutMs) {
|
|
110
116
|
return new Promise((resolve, reject) => {
|
|
@@ -115,7 +121,8 @@ function installPackages(prefix, packageNames, proxyUri, timeoutMs) {
|
|
|
115
121
|
let stdout = '';
|
|
116
122
|
let stderr = '';
|
|
117
123
|
|
|
118
|
-
const
|
|
124
|
+
const ops = '--no-save --no-package-lock --no-prune --prefer-offline --no-audit --legacy-peer-deps --progress=false'.split(' ');
|
|
125
|
+
const npmInstall = spawn('node', [localNpmLocation, 'i', '--prefix', prefix, ...ops, ...packageNames, ...proxyFlag], envVars);
|
|
119
126
|
npmInstall.stderr.pipe(process.stderr);
|
|
120
127
|
npmInstall.stdout.pipe(process.stdout);
|
|
121
128
|
|
|
@@ -54,7 +54,7 @@ describe('npmWrapper', () => {
|
|
|
54
54
|
fakeChildProcess.exec.yields(undefined, []); //resolve without errors
|
|
55
55
|
const cwd = '/some/dir';
|
|
56
56
|
const pkg = 'some-package';
|
|
57
|
-
const expectedCmd = 'npm i some-package --no-save --no-package-lock --no-prune --prefer-offline --no-audit --progress=false';
|
|
57
|
+
const expectedCmd = 'npm i some-package --no-save --no-package-lock --no-prune --prefer-offline --no-audit --legacy-peer-deps --progress=false';
|
|
58
58
|
const expectedExecParams = { cwd };
|
|
59
59
|
|
|
60
60
|
await npmWrapper.installPackageLocally(cwd, pkg);
|
|
@@ -567,6 +567,27 @@ function buildSeleniumOptions(browserOptions, testName, testRunConfig, gridInfo,
|
|
|
567
567
|
|
|
568
568
|
_.merge(opts.desiredCapabilities, browserOptions.seleniumCapsFileContent);
|
|
569
569
|
|
|
570
|
+
try {
|
|
571
|
+
/**
|
|
572
|
+
* Targeted custom capabilities can be added to the desired capabilities object via the addCustomCapabilities FF.
|
|
573
|
+
* No targeting: { selenium_version: '3.141.59' }
|
|
574
|
+
* One level targeting (either grid provider, host, browser name or browser version): { "devicefarm": { selenium_version: '3.141.59' } }
|
|
575
|
+
* Two level targeting: { "internet explorer": { "11": { selenium_version: '3.141.59' } } }
|
|
576
|
+
*/
|
|
577
|
+
const hostToProvider = { 'hub.lambdatest.com': 'lambdatest', 'public-grid.testim.io': 'testim', 'testgrid-devicefarm.us-west-2.amazonaws.com': 'devicefarm' };
|
|
578
|
+
const byGrid = (capabilities) => capabilities[gridInfo.provider] || capabilities[opts.host] || capabilities[hostToProvider[opts.host]];
|
|
579
|
+
const getTargetingGroup = (capabilities) => byGrid(capabilities) || capabilities[opts.desiredCapabilities.browserName] || capabilities[opts.desiredCapabilities.version] || capabilities || {};
|
|
580
|
+
const capabilities = JSON.parse(featureFlags.flags.addCustomCapabilities.getValue() || '{}');
|
|
581
|
+
const customCapabilities = getTargetingGroup(getTargetingGroup(capabilities));
|
|
582
|
+
|
|
583
|
+
if (Object.keys(customCapabilities).length) {
|
|
584
|
+
logger.info(`Adding custom capabilities: ${JSON.stringify(customCapabilities)}`);
|
|
585
|
+
Object.assign(opts.desiredCapabilities, customCapabilities);
|
|
586
|
+
}
|
|
587
|
+
} catch (e) {
|
|
588
|
+
logger.error(`Failed to load custom capabilities: ${e.message}`, { customCapabilities: featureFlags.flags.addCustomCapabilities.getValue() });
|
|
589
|
+
}
|
|
590
|
+
|
|
570
591
|
if (isDFGrid(gridInfo) && opts.desiredCapabilities && !opts.capabilities) {
|
|
571
592
|
convertToNewCapabilitiesFormat(opts.desiredCapabilities);
|
|
572
593
|
opts.capabilities = { alwaysMatch: opts.desiredCapabilities, firstMatch: [{}] };
|