@testim/testim-cli 3.228.0 → 3.231.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.
@@ -1,4 +1,4 @@
1
- "use strict";
1
+ 'use strict';
2
2
 
3
3
  const path = require('path');
4
4
  const findRoot = require('find-root');
@@ -7,16 +7,17 @@ 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');
10
- const logger = require('../../../commons/logger').getLogger("cli-service");
10
+ 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
+
15
+ let workerThreads;
15
16
 
16
17
  config.set({
17
18
  basepath: {
18
- node: __dirname
19
- }
19
+ node: __dirname,
20
+ },
20
21
  });
21
22
 
22
23
  const transactions = {};
@@ -32,19 +33,19 @@ function convertWindowsBackslash(input) {
32
33
  return input.replace(/\\/g, '/');
33
34
  }
34
35
 
35
- function runCode(transactionId, incomingParams, context, code, packageLocalLocations = {}, timeout, fileDataUrl) {
36
+ function runCode(transactionId, incomingParams, context, code, packageLocalLocations = {}, timeout = undefined, fileDataUrl = undefined) {
36
37
  const requireCode = Object.keys(packageLocalLocations).reduce((all, pMember) => {
37
38
  all += `
38
39
  var ${pMember} = require('${convertWindowsBackslash(packageLocalLocations[pMember])}');
39
40
  `;
40
41
  return all;
41
- }, "");
42
+ }, '');
42
43
 
43
44
  if (fileDataUrl === 'data:') { // fix chrome/safari bug that creates malformed datauri for empty files
44
45
  fileDataUrl = 'data:,';
45
46
  }
46
47
 
47
- const fileDataUrlToFileBuffer = !fileDataUrl ? `var fileBuffer = null;` :
48
+ const fileDataUrlToFileBuffer = !fileDataUrl ? 'var fileBuffer = null;' :
48
49
  `
49
50
  ${dataUriToBuffer.toString()}
50
51
  var fileBuffer = dataUriToBuffer('${fileDataUrl}');
@@ -212,7 +213,7 @@ function runCode(transactionId, incomingParams, context, code, packageLocalLocat
212
213
  thread
213
214
  .send({ incomingParams, context, code })
214
215
  .on('message', message => {
215
- const messageWithLogs = Object.assign({}, message, { tstConsoleLogs: testimConsoleLogDataAggregates })
216
+ const messageWithLogs = Object.assign({}, message, { tstConsoleLogs: testimConsoleLogDataAggregates });
216
217
  logger.debug('Run code worker response', { messageWithLogs, transactionId });
217
218
  resolve(messageWithLogs);
218
219
  })
@@ -220,7 +221,7 @@ function runCode(transactionId, incomingParams, context, code, packageLocalLocat
220
221
  testimConsoleLogDataAggregates.push(logMessage);
221
222
  })
222
223
  .on('error', (err) => {
223
- if (err.message === "malformed data: URI") {
224
+ if (err.message === 'malformed data: URI') {
224
225
  logger.error('Run code worker error', { err, transactionId, fileDataUrl });
225
226
  } else {
226
227
  logger.error('Run code worker error', { err, transactionId });
@@ -233,9 +234,9 @@ function runCode(transactionId, incomingParams, context, code, packageLocalLocat
233
234
  resultValue: err && err.toString(),
234
235
  exports: {},
235
236
  exportsTest: {},
236
- exportsGlobal: {}
237
+ exportsGlobal: {},
237
238
  },
238
- success: false
239
+ success: false,
239
240
  });
240
241
  })
241
242
  .on('exit', () => {
@@ -243,7 +244,7 @@ function runCode(transactionId, incomingParams, context, code, packageLocalLocat
243
244
  });
244
245
  }).timeout(timeout)
245
246
  .catch(Promise.TimeoutError, err => {
246
- logger.warn("timeout to run code", { transactionId, err });
247
+ logger.warn('timeout to run code', { transactionId, err });
247
248
  return Promise.resolve({
248
249
  tstConsoleLogs: testimConsoleLogDataAggregates,
249
250
  status: 'failed',
@@ -251,23 +252,301 @@ function runCode(transactionId, incomingParams, context, code, packageLocalLocat
251
252
  resultValue: err && err.toString(),
252
253
  exports: {},
253
254
  exportsTest: {},
254
- exportsGlobal: {}
255
+ exportsGlobal: {},
255
256
  },
256
- success: false
257
+ success: false,
257
258
  });
258
259
  })
259
260
  .finally(() => thread && thread.kill());
260
261
  }
261
262
 
262
- function removeFolder(installFolder) {
263
- return new Promise(resolve => {
264
- return fs.remove(installFolder)
265
- .then(resolve)
266
- .catch(err => {
267
- logger.warn(`failed to remove install npm packages folder`, { err });
268
- return resolve();
263
+ function requireOrImportMethod(path) {
264
+ try {
265
+ return { sync: true, lib: require(path) };
266
+ } catch (err) {
267
+ if (err.code === 'ERR_REQUIRE_ESM') {
268
+ const fs = require('fs');
269
+ const pathModule = require('path');
270
+
271
+ const lib = fs.promises.readFile(`${path}${pathModule.sep}package.json`).then(file => {
272
+ const packageJson = JSON.parse(file);
273
+ const fullPath = pathModule.join(path, packageJson.main || `${pathModule.sep}index.js`);
274
+ return import(fullPath);
275
+ });
276
+
277
+ return { sync: false, lib };
278
+ }
279
+ throw err;
280
+ }
281
+ }
282
+
283
+ function runCodeWithWorkerThread(transactionId, incomingParams, context, code, packageLocalLocations = {}, timeout = undefined, fileDataUrl = undefined) {
284
+ // technically shouldn't happen, but better safe than sorry.
285
+ if (!workerThreads) {
286
+ workerThreads = require('worker_threads');
287
+ }
288
+ const { Worker } = workerThreads;
289
+ const requireCode = Object.keys(packageLocalLocations).reduce((all, pMember) => {
290
+ all += `
291
+ var res = requireOrImportMethod('${convertWindowsBackslash(packageLocalLocations[pMember])}');
292
+ if (res.sync) {
293
+ var ${pMember} = res.lib;
294
+ } else {
295
+ var ${pMember} = await res.lib;
296
+ }
297
+ `;
298
+ return all;
299
+ }, '');
300
+
301
+ if (fileDataUrl === 'data:') { // fix chrome/safari bug that creates malformed datauri for empty files
302
+ fileDataUrl = 'data:,';
303
+ }
304
+
305
+ const fileDataUrlToFileBuffer = !fileDataUrl ? 'var fileBuffer = null;' :
306
+ `
307
+ ${dataUriToBuffer.toString()}
308
+ var fileBuffer = dataUriToBuffer('${fileDataUrl}');
309
+ `;
310
+
311
+ function constructWithArguments(constructor, args) {
312
+ function F() {
313
+ return constructor.apply(this, args);
314
+ }
315
+
316
+ F.prototype = constructor.prototype;
317
+ return new F();
318
+ }
319
+
320
+ //https://github.com/anseki/console-substitute
321
+ // note that this method is a bit different than the one in the non-worker one.
322
+ const consoleOverride = `
323
+ const getMessage = arguments => {
324
+ const args = Array.prototype.slice.call(arguments);
325
+ let message = args.shift() + '';
326
+ if (!args.length) {
327
+ return message;
328
+ }
329
+ message = message.replace(/%([odifs])/g, function (s, param) {
330
+ // Formatting is not yet supported.
331
+ var arg;
332
+ if (!args.length) {
333
+ return '';
334
+ }
335
+ arg = args.shift();
336
+ if (param === 'o') {
337
+ return arg + '';
338
+ } else if (param === 'd' || param === 'i') {
339
+ arg = typeof arg === 'boolean' ? (arg ? 1 : 0) : parseInt(arg, 10);
340
+ return isNaN(arg) ? '0' : arg + '';
341
+ } else if (param === 'f') {
342
+ arg = typeof arg === 'boolean' ? (arg ? 1 : 0) : parseFloat(arg);
343
+ return isNaN(arg) ? '0.000000' : arg.toFixed(6) + '';
344
+ } else if (param === 's') {
345
+ return arg + '';
346
+ }
347
+ });
348
+ if (message) {
349
+ args.unshift(message);
350
+ }
351
+ return args.join(' ').replace(/\\s*$/, ' ');
352
+ };
353
+
354
+ const pushNewMessage = (method, consoleArgs) => {
355
+ const message = getMessage(consoleArgs);
356
+ parentPort.postMessage({
357
+ action: 'progress',
358
+ data: {
359
+ method,
360
+ msg: message,
361
+ timestamp: Date.now(),
362
+ }
363
+ });
364
+ };
365
+
366
+ ["log", "info", "warn", "error", "debug"].forEach(function (method) {
367
+ const nativeMethod = console[method];
368
+ const oldMethod = nativeMethod && nativeMethod.bind(console);
369
+ console[method] = function () {
370
+ pushNewMessage(method, arguments);
371
+ oldMethod && oldMethod.apply(console, arguments);
372
+ };
373
+ });
374
+ `;
375
+
376
+ const injectCode = `
377
+ function injectCode(params, args, incomingParams, context, code) {
378
+ ${constructWithArguments.toString()}
379
+
380
+ var resolve = function (result) {
381
+ parentPort.postMessage({
382
+ action: 'finish',
383
+ data: {
384
+ status: 'done',
385
+ result: result,
386
+ success: true,
387
+ }
388
+ });
389
+ };
390
+ var reject = function (result) {
391
+ parentPort.postMessage({
392
+ action: 'finish',
393
+ data: {
394
+ status: 'failed',
395
+ result: result,
396
+ success: false,
397
+ }
398
+ });
399
+ };
400
+
401
+ try {
402
+ params.push(code);
403
+ const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor;
404
+ var functionToRun = constructWithArguments(AsyncFunction, params);
405
+
406
+ var result = functionToRun.apply(null, args);
407
+ if (isPromise(result)) {
408
+ result.then(function (res) {
409
+ resolve({
410
+ resultValue: res,
411
+ exports: exportedData,
412
+ exportsTest: exportedTestData,
413
+ exportsGlobal: exportedGlobalData
414
+ });
415
+ }).catch(function (err) {
416
+ reject({
417
+ resultValue: err && err.toString(),
418
+ exports: exportedData,
419
+ exportsTest: exportedTestData,
420
+ exportsGlobal: exportedGlobalData
421
+ });
422
+ });
423
+ } else {
424
+ resolve({
425
+ resultValue: result,
426
+ exports: exportedData,
427
+ exportsTest: exportedTestData,
428
+ exportsGlobal: exportedGlobalData
429
+ });
430
+ }
431
+ } catch (e) {
432
+ reject({
433
+ resultValue: e && e.toString(),
434
+ exports: exportedData,
435
+ exportsTest: exportedTestData,
436
+ exportsGlobal: exportedGlobalData
437
+ });
438
+ }
439
+ }
440
+ `;
441
+
442
+ const runFn = `
443
+ (async function() {
444
+ const { parentPort } = require('worker_threads');
445
+ ${requireOrImportMethod}
446
+
447
+ // requireCode will set async to be true if needed.
448
+ ${requireCode}
449
+
450
+ ${fileDataUrlToFileBuffer}
451
+
452
+ ${consoleOverride}
453
+
454
+ ${utils.isPromise.toString()}
455
+
456
+ parentPort.once('message', input => {
457
+ const {incomingParams, context, code} = input;
458
+
459
+ var exportedData = {};
460
+ var exportedTestData = {};
461
+ var exportedGlobalData = {};
462
+
463
+ var params = ["context"]
464
+ .concat(incomingParams.as.functionParameters)
465
+ .concat(${JSON.stringify(Object.keys(packageLocalLocations))})
466
+ .concat(['exports', 'exportsTest', 'exportsGlobal']);
467
+
468
+ var args = [context]
469
+ .concat(incomingParams.as.functionArguments)
470
+ .concat([${Object.keys(packageLocalLocations).join(',')}])
471
+ .concat([exportedData, exportedTestData, exportedGlobalData]);
472
+
473
+ if(fileBuffer) {
474
+ params = params.concat(['fileBuffer']);
475
+ args = args.concat([fileBuffer]);
476
+ }
477
+
478
+ ${injectCode}
479
+
480
+ injectCode(params, args, incomingParams, context, code);
269
481
  });
482
+ })();
483
+ `;
484
+
485
+ const testimConsoleLogDataAggregates = [];
486
+ const thread = new Worker(runFn, {
487
+ eval: true,
270
488
  });
489
+ return new Promise((resolve) => {
490
+ thread
491
+ .on('message', message => {
492
+ if (message.action === 'finish') {
493
+ const { data } = message;
494
+ const messageWithLogs = Object.assign({}, data, { tstConsoleLogs: testimConsoleLogDataAggregates });
495
+ logger.debug('Run code worker response', { messageWithLogs, transactionId });
496
+ resolve(messageWithLogs);
497
+ } else if (message.action === 'progress') {
498
+ testimConsoleLogDataAggregates.push(message.data);
499
+ }
500
+ })
501
+ .on('error', (err) => {
502
+ if (err.message === 'malformed data: URI') {
503
+ logger.error('Run code worker error', { err, transactionId, fileDataUrl });
504
+ } else {
505
+ logger.error('Run code worker error', { err, transactionId });
506
+ }
507
+
508
+ resolve({
509
+ tstConsoleLogs: testimConsoleLogDataAggregates,
510
+ status: 'failed',
511
+ result: {
512
+ resultValue: err && err.toString(),
513
+ exports: {},
514
+ exportsTest: {},
515
+ exportsGlobal: {},
516
+ },
517
+ success: false,
518
+ });
519
+ })
520
+ .on('exit', () => {
521
+ logger.debug('Run code worker has been terminated', { transactionId });
522
+ });
523
+ // context can contain methods and proxies which cannot pass.
524
+ thread.postMessage({ incomingParams, context: JSON.parse(JSON.stringify(context)), code });
525
+ }).timeout(timeout)
526
+ .catch(Promise.TimeoutError, err => {
527
+ logger.warn('timeout to run code', { transactionId, err });
528
+ return Promise.resolve({
529
+ tstConsoleLogs: testimConsoleLogDataAggregates,
530
+ status: 'failed',
531
+ result: {
532
+ resultValue: err && err.toString(),
533
+ exports: {},
534
+ exportsTest: {},
535
+ exportsGlobal: {},
536
+ },
537
+ success: false,
538
+ });
539
+ })
540
+ .finally(() => thread && thread.terminate());
541
+ }
542
+
543
+ function removeFolder(installFolder) {
544
+ return new Promise(resolve => fs.remove(installFolder)
545
+ .then(resolve)
546
+ .catch(err => {
547
+ logger.warn('failed to remove install npm packages folder', { err });
548
+ return resolve();
549
+ }));
271
550
  }
272
551
 
273
552
  function cleanPackages(transactionId) {
@@ -291,31 +570,14 @@ function getTransactionId(stepResultId, testResultId, stepId, retryIndex) {
291
570
  return `${testResultId}_${stepId}_${stepResultId}_${retryIndex}`;
292
571
  }
293
572
 
294
- function mapNpmInstallDataToInstallData(npmInstallData, packageData) {
295
- const npmInstallDataObject = npmInstallData.reduce((obj, [packageFullName, packageLocalLocation]) => {
296
- const strudelIndex = packageFullName.lastIndexOf('@');
297
- const npmInstalledPackageName = packageFullName.substr(0, strudelIndex);
298
- obj[npmInstalledPackageName] = {
299
- packageFullName,
300
- packageLocalLocation
301
- };
302
- return obj;
303
- }, {});
304
- return packageData.map(data => {
305
- const npmPackageName = data.packageName;
306
- return Object.assign({}, npmInstallDataObject[npmPackageName], data);
307
- });
308
- }
309
-
310
573
  function installPackage(stepId, testResultId, retryIndex, packageData, stepResultId, timeout) {
311
574
  const transactionId = getTransactionId(stepResultId, testResultId, stepId, retryIndex);
312
575
  return runNpmInstall(transactionId, packageData, timeout)
313
576
  .then(({ data, installFolder }) => {
314
577
  transactions[transactionId] = transactions[transactionId] || {};
315
578
  transactions[transactionId].installFolder = installFolder;
316
- return Promise.resolve(data);
579
+ return data;
317
580
  });
318
-
319
581
  }
320
582
 
321
583
  function runCodeWithPackages(code, stepId, incomingParams, context, testResultId, retryIndex, stepResultId, timeout, fileDataUrl, s3filepath) {
@@ -333,8 +595,20 @@ function runCodeWithPackages(code, stepId, incomingParams, context, testResultId
333
595
  if (s3fileDataUrl) {
334
596
  fileDataUrl = s3fileDataUrl;
335
597
  }
336
- }).then(() => runCode(transactionId, incomingParams, context, code, packageLocalLocations, timeout, fileDataUrl))
337
- .then(res => Object.assign({}, res, { nodeVersion: process.version }))
598
+ }).then(() => {
599
+ if (typeof workerThreads === 'undefined') {
600
+ try {
601
+ workerThreads = require('worker_threads');
602
+ } catch (err) {
603
+ workerThreads = false;
604
+ }
605
+ }
606
+
607
+ if (workerThreads && featureFlags.flags.enableWorkerThreadsCliCodeExecution.isEnabled()) {
608
+ return runCodeWithWorkerThread(transactionId, incomingParams, context, code, packageLocalLocations, timeout, fileDataUrl);
609
+ }
610
+ return runCode(transactionId, incomingParams, context, code, packageLocalLocations, timeout, fileDataUrl);
611
+ }).then(res => Object.assign({}, res, { nodeVersion: process.version }))
338
612
  .finally(() => cleanPackages(transactionId));
339
613
  }
340
614
 
@@ -343,6 +617,9 @@ function runNpmInstall(transactionId, packageData, timeout) {
343
617
  const localPackageInstallFolder = getLocalPackageInstallFolder();
344
618
  const installFolder = path.join(localPackageInstallFolder, `/${transactionId}`);
345
619
  const proxyUri = global.proxyUri;
620
+
621
+ // while correct, everything is in a try/catch so it should be fine.
622
+ // eslint-disable-next-line no-async-promise-executor
346
623
  return new Promise(async (resolve, reject) => {
347
624
  let output = '';
348
625
  try {
@@ -358,8 +635,8 @@ function runNpmInstall(transactionId, packageData, timeout) {
358
635
  const packageLocalLocation = path.resolve(installFolder, 'node_modules', pData.packageName);
359
636
  return Object.assign({}, pData, {
360
637
  packageFullName,
361
- packageLocalLocation
362
- })
638
+ packageLocalLocation,
639
+ });
363
640
  });
364
641
 
365
642
  resolve({ data: packageDataWithVersions, installFolder });
@@ -367,11 +644,10 @@ function runNpmInstall(transactionId, packageData, timeout) {
367
644
  logger.warn('npm package install failed', { transactionId, err });
368
645
  reject(err);
369
646
  }
370
-
371
647
  })
372
648
  .timeout(timeout)
373
649
  .catch(Promise.TimeoutError, err => {
374
- logger.warn("timeout to install package", { packages, transactionId, err, timeout });
650
+ logger.warn('timeout to install package', { packages, transactionId, err, timeout });
375
651
  throw err;
376
652
  });
377
653
  }
@@ -397,5 +673,5 @@ function cleanLocalPackageInstallFolder() {
397
673
  module.exports = {
398
674
  runCodeWithPackages,
399
675
  installPackage,
400
- cleanLocalPackageInstallFolder
676
+ cleanLocalPackageInstallFolder,
401
677
  };
@@ -56,6 +56,7 @@ class FeatureFlagsService {
56
56
  applitoolsNewIntegration: new Rox.Flag(),
57
57
  testNamesToBeforeSuiteHook: new Rox.Flag(),
58
58
  addCustomCapabilities: new Rox.Variant('{}'),
59
+ enableWorkerThreadsCliCodeExecution: new Rox.Flag(true),
59
60
  };
60
61
  Rox.register('default', this.flags);
61
62
  }