@testim/testim-cli 3.266.0 → 3.267.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/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/runners/ParallelWorkerManager.js +66 -12
- package/runners/TestPlanRunner.js +2 -4
- package/services/gridService.js +48 -36
- package/services/gridService.test.js +11 -18
- package/testRunStatus.js +9 -1
- package/workers/BaseWorker.js +83 -29
- package/workers/BaseWorker.test.js +1 -1
- package/workers/WorkerAppium.js +0 -5
- package/workers/WorkerExtensionSingleBrowser.js +5 -0
- package/workers/WorkerSelenium.js +16 -7
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@testim/testim-cli",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.267.0",
|
|
4
4
|
"lockfileVersion": 2,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@testim/testim-cli",
|
|
9
|
-
"version": "3.
|
|
9
|
+
"version": "3.267.0",
|
|
10
10
|
"license": "Proprietary",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@applitools/eyes-sdk-core": "13.11.21",
|
package/package.json
CHANGED
|
@@ -14,11 +14,18 @@ const { StopRunOnError } = require('../errors');
|
|
|
14
14
|
|
|
15
15
|
require('../player/webdriver'); // preload
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* @template {unknown[]} T
|
|
19
|
+
* @typedef {T extends [infer H, ...infer R] ? R : T} RemoveFirst
|
|
20
|
+
*/
|
|
21
|
+
|
|
17
22
|
class ParallelWorkerManager {
|
|
23
|
+
/** @param {string=} customExtensionLocalLocation */
|
|
18
24
|
constructor(customExtensionLocalLocation) {
|
|
19
25
|
this.customExtensionLocalLocation = customExtensionLocalLocation;
|
|
20
26
|
}
|
|
21
27
|
|
|
28
|
+
/** @param {typeof CLI_MODE[keyof typeof CLI_MODE]} mode */
|
|
22
29
|
getWorkerType(mode) {
|
|
23
30
|
switch (mode) {
|
|
24
31
|
case CLI_MODE.SELENIUM:
|
|
@@ -33,6 +40,12 @@ class ParallelWorkerManager {
|
|
|
33
40
|
}
|
|
34
41
|
}
|
|
35
42
|
|
|
43
|
+
/**
|
|
44
|
+
* @param {number} count
|
|
45
|
+
* @param {ExecutionQueue} queue
|
|
46
|
+
* @param {typeof CLI_MODE[keyof typeof CLI_MODE]} mode
|
|
47
|
+
* @param {...RemoveFirst<ConstructorParameters<typeof import('../workers/BaseWorker')>>} args
|
|
48
|
+
*/
|
|
36
49
|
createWorkers(count, queue, mode, ...args) {
|
|
37
50
|
const Worker = this.getWorkerType(mode);
|
|
38
51
|
const createWorker = () => {
|
|
@@ -47,6 +60,17 @@ class ParallelWorkerManager {
|
|
|
47
60
|
return Array.from(new Array(count), createWorker);
|
|
48
61
|
}
|
|
49
62
|
|
|
63
|
+
/**
|
|
64
|
+
* @param {import('./TestPlanRunner').ExecutionList} testList
|
|
65
|
+
* @param {import('../testRunStatus')} testStatus
|
|
66
|
+
* @param {string} executionId
|
|
67
|
+
* @param {string} executionName
|
|
68
|
+
* @param {import('../runOptions').RunnerOptions} options
|
|
69
|
+
* @param {string} branchToUse
|
|
70
|
+
* @param {ReturnType<typeof testimCustomToken['getTokenV3UserData']>} authData
|
|
71
|
+
* @param {number} workerCount
|
|
72
|
+
* @param {boolean} stopOnError
|
|
73
|
+
*/
|
|
50
74
|
async runTests(testList, testStatus, executionId, executionName, options, branchToUse, authData, workerCount, stopOnError) {
|
|
51
75
|
if (testList && testList.length === 0) {
|
|
52
76
|
return undefined;
|
|
@@ -72,6 +96,13 @@ class ParallelWorkerManager {
|
|
|
72
96
|
const lightweightMode = options.lightweightMode;
|
|
73
97
|
const sessionType = utils.getSessionType(options);
|
|
74
98
|
|
|
99
|
+
/**
|
|
100
|
+
* @param {number} wid
|
|
101
|
+
* @param {string} testId
|
|
102
|
+
* @param {string} resultId
|
|
103
|
+
* @param {boolean | undefined} isRerun
|
|
104
|
+
* @param {`${number}:${number}`} testRetryKey
|
|
105
|
+
*/
|
|
75
106
|
const onTestStarted = (wid, testId, resultId, isRerun, testRetryKey) => {
|
|
76
107
|
runningTests++;
|
|
77
108
|
analyticsService.analyticsTestStart({
|
|
@@ -93,6 +124,13 @@ class ParallelWorkerManager {
|
|
|
93
124
|
return testStatus.testStartAndReport(wid, executionId, resultId, isRerun, testRetryKey);
|
|
94
125
|
};
|
|
95
126
|
|
|
127
|
+
/**
|
|
128
|
+
* @param {number} wid
|
|
129
|
+
* @param {string} testId
|
|
130
|
+
* @param {*} testResult
|
|
131
|
+
* @param {string} sessionId
|
|
132
|
+
* @param {boolean | undefined} isRerun
|
|
133
|
+
*/
|
|
96
134
|
const onTestCompleted = async (wid, testId, testResult, sessionId, isRerun) => {
|
|
97
135
|
runningTests--;
|
|
98
136
|
const update = {};
|
|
@@ -135,7 +173,7 @@ class ParallelWorkerManager {
|
|
|
135
173
|
.catch(err => logger.error('testEndAndReport threw an error', { err }));
|
|
136
174
|
|
|
137
175
|
if (isRerun) {
|
|
138
|
-
return
|
|
176
|
+
return;
|
|
139
177
|
}
|
|
140
178
|
combinedTestResults[testResult.resultId] = testResult;
|
|
141
179
|
analyticsService.analyticsTestEnd({
|
|
@@ -163,11 +201,13 @@ class ParallelWorkerManager {
|
|
|
163
201
|
const completedTests = Object.keys(combinedTestResults).length;
|
|
164
202
|
if (completedTests === testCount || (stoppedOnError && runningTests === 0)) {
|
|
165
203
|
resolve(combinedTestResults);
|
|
166
|
-
return undefined;
|
|
167
204
|
}
|
|
168
|
-
return undefined;
|
|
169
205
|
};
|
|
170
206
|
|
|
207
|
+
/**
|
|
208
|
+
* @param {number} wid
|
|
209
|
+
* @param {{ name: string; testId: string; resultId: string; runnerStatus: 'SKIPPED'; testStatus: 'quarantine' }} testResult
|
|
210
|
+
*/
|
|
171
211
|
const onTestIgnored = (wid, testResult) => {
|
|
172
212
|
combinedTestResults[testResult.resultId] = testResult;
|
|
173
213
|
testStatus.onTestIgnored(wid, testResult.resultId);
|
|
@@ -178,7 +218,7 @@ class ParallelWorkerManager {
|
|
|
178
218
|
}
|
|
179
219
|
};
|
|
180
220
|
|
|
181
|
-
const onGridSlot = (
|
|
221
|
+
const onGridSlot = (resultId, gridInfo) => testStatus.onGridSlot(resultId, gridInfo);
|
|
182
222
|
|
|
183
223
|
options.userData = {
|
|
184
224
|
loginData: Object.assign({}, testimCustomToken.getTokenV3UserData(), {
|
|
@@ -191,14 +231,24 @@ class ParallelWorkerManager {
|
|
|
191
231
|
servicesUrl: config.SERVICES_HOST,
|
|
192
232
|
};
|
|
193
233
|
perf.log('in localStrategy before createWorker');
|
|
194
|
-
this.createWorkers(
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
234
|
+
this.createWorkers(
|
|
235
|
+
workerCount,
|
|
236
|
+
executionQueue,
|
|
237
|
+
options.mode,
|
|
238
|
+
options,
|
|
239
|
+
this.customExtensionLocalLocation,
|
|
240
|
+
executionId,
|
|
241
|
+
onTestStarted,
|
|
242
|
+
onTestCompleted,
|
|
243
|
+
onGridSlot,
|
|
244
|
+
onTestIgnored,
|
|
245
|
+
).forEach((worker, index) => {
|
|
246
|
+
perf.log('before schedule worker.run after createWorkers');
|
|
247
|
+
schedule(() => {
|
|
248
|
+
perf.log('right before worker.run');
|
|
249
|
+
worker.run();
|
|
250
|
+
}, index);
|
|
251
|
+
});
|
|
202
252
|
});
|
|
203
253
|
|
|
204
254
|
try {
|
|
@@ -215,6 +265,10 @@ class ParallelWorkerManager {
|
|
|
215
265
|
}
|
|
216
266
|
}
|
|
217
267
|
|
|
268
|
+
/**
|
|
269
|
+
* @param {Function} fn
|
|
270
|
+
* @param {number} index
|
|
271
|
+
*/
|
|
218
272
|
function schedule(fn, index) {
|
|
219
273
|
if (index === 0) {
|
|
220
274
|
fn();
|
|
@@ -26,9 +26,7 @@ const TDK_CHILD_RESULTS_TIMEOUT = 1000 * 60 * 5;
|
|
|
26
26
|
/** @typedef {Awaited<ReturnType<typeof getSuite>>['tests'][number]} ExecutionList */
|
|
27
27
|
|
|
28
28
|
class TestPlanRunner {
|
|
29
|
-
/**
|
|
30
|
-
* @param {string=} customExtensionLocalLocation
|
|
31
|
-
*/
|
|
29
|
+
/** @param {string=} customExtensionLocalLocation */
|
|
32
30
|
constructor(customExtensionLocalLocation) {
|
|
33
31
|
this.workerManager = new ParallelWorkerManager(customExtensionLocalLocation);
|
|
34
32
|
this.startTime = Date.now();
|
|
@@ -115,7 +113,7 @@ class TestPlanRunner {
|
|
|
115
113
|
}
|
|
116
114
|
const { runKey: apiKey, url: serverUrl } = applitoolsIntegrationData;
|
|
117
115
|
const tmpSDK = require('@applitools/eyes-sdk-core').makeSDK({ name: 'Testim.io', version: '4.0.0', spec: {} });
|
|
118
|
-
await tmpSDK.
|
|
116
|
+
await tmpSDK.closeBatches({ settings: { batchIds: [executionId], serverUrl, apiKey } });
|
|
119
117
|
} catch (err) {
|
|
120
118
|
// If a batch with this name did not exist, do not log an error.
|
|
121
119
|
if (err.message && (err.message.startsWith('Request failed with status code 404') || err.message.startsWith('no batchIds were set'))) {
|
package/services/gridService.js
CHANGED
|
@@ -59,38 +59,47 @@ function getSerializableObject(grid) {
|
|
|
59
59
|
};
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
62
|
+
/**
|
|
63
|
+
* @template {{ status: string; code?: string; grid: * }} T
|
|
64
|
+
* @param {string} projectId
|
|
65
|
+
* @param {string} companyId
|
|
66
|
+
* @param {number} workerId
|
|
67
|
+
* @param {string} browser
|
|
68
|
+
* @param {() => Promise<T>} getFun
|
|
69
|
+
*/
|
|
70
|
+
async function handleGetGridResponse(projectId, companyId, workerId, browser, getFun) {
|
|
71
|
+
/** @type {T} */
|
|
72
|
+
let res;
|
|
73
|
+
try {
|
|
74
|
+
res = await getFun();
|
|
75
|
+
} catch (err) {
|
|
76
|
+
logger.error('failed to get grid', { projectId, companyId, err });
|
|
77
|
+
throw new Error(gridMessages.UNKNOWN);
|
|
78
|
+
}
|
|
79
|
+
logger.info('get grid info', Object.assign({}, res, { projectId, companyId }));
|
|
80
|
+
const isSuccess = () => res.status === 'success';
|
|
81
|
+
const isError = () => res.status === 'error' && res.code;
|
|
82
|
+
if (!res || (!isError() && !isSuccess())) {
|
|
83
|
+
logger.error('invalid response - get grid', { res });
|
|
84
|
+
throw new Error(gridMessages.UNKNOWN);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (isSuccess()) {
|
|
88
|
+
const serGrid = getSerializableObject(res.grid);
|
|
89
|
+
module.exports.addItemToGridCache(workerId, companyId, serGrid.gridId, serGrid.slotId, browser);
|
|
90
|
+
return serGrid;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (isError() && res.code === 'not-found') {
|
|
94
|
+
throw new GridError(gridMessages.NOT_FOUND);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (isError() && res.code === 'no-available-slot') {
|
|
98
|
+
throw new GridError(`Failed to run test on ${browser} - concurrency limit reached`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
logger.error('invalid code error response - get grid', { res });
|
|
102
|
+
throw new GridError(gridMessages.UNKNOWN);
|
|
94
103
|
}
|
|
95
104
|
|
|
96
105
|
function addItemToGridCache(workerId, companyId, gridId, slotId, browser) {
|
|
@@ -264,7 +273,13 @@ async function getGridData(options) {
|
|
|
264
273
|
throw new GridError('Missing host or grid configuration');
|
|
265
274
|
}
|
|
266
275
|
|
|
267
|
-
|
|
276
|
+
/**
|
|
277
|
+
* @param {string} browser
|
|
278
|
+
* @param {string} executionId
|
|
279
|
+
* @param {import('../runOptions')} options
|
|
280
|
+
* @param {number} workerId
|
|
281
|
+
*/
|
|
282
|
+
async function getGridSlot(browser, executionId, options, workerId) {
|
|
268
283
|
const getGridDataFromServer = async () => {
|
|
269
284
|
const { host, project, grid, gridId, useLocalChromeDriver, useChromeLauncher, company = {} } = options;
|
|
270
285
|
const companyId = company.companyId;
|
|
@@ -284,9 +299,6 @@ async function getGridSlot(browser, executionId, testResultId, onGridSlot, optio
|
|
|
284
299
|
};
|
|
285
300
|
|
|
286
301
|
const gridInfo = await getGridDataFromServer();
|
|
287
|
-
|
|
288
|
-
await onGridSlot(executionId, testResultId, gridInfo);
|
|
289
|
-
|
|
290
302
|
return gridInfo;
|
|
291
303
|
}
|
|
292
304
|
|
|
@@ -169,14 +169,12 @@ describe('gridService', () => {
|
|
|
169
169
|
describe('getGridSlot', () => {
|
|
170
170
|
let getGridByIdStub;
|
|
171
171
|
let getGridByNameStub;
|
|
172
|
-
let onGridSlot;
|
|
173
172
|
let addItemToGridCacheStub;
|
|
174
173
|
|
|
175
174
|
beforeEach(() => {
|
|
176
175
|
getGridByIdStub = sinon.stub(servicesApi, 'getGridById').resolves({ grid: { gridId: 'gridId', type: 'gridId' }, status: 'success' });
|
|
177
176
|
getGridByNameStub = sinon.stub(servicesApi, 'getGridByName').resolves({ grid: { gridId: 'gridId', type: 'gridName' }, status: 'success' });
|
|
178
177
|
addItemToGridCacheStub = sinon.stub(gridService, 'addItemToGridCache').callThrough();
|
|
179
|
-
onGridSlot = sinon.stub().resolves();
|
|
180
178
|
});
|
|
181
179
|
afterEach(() => {
|
|
182
180
|
getGridByIdStub.restore();
|
|
@@ -185,82 +183,77 @@ describe('gridService', () => {
|
|
|
185
183
|
});
|
|
186
184
|
|
|
187
185
|
it('should not access server when using useLocalChromeDriver flag', async () => {
|
|
188
|
-
const slot = await gridService.getGridSlot('browser', 'executionId',
|
|
186
|
+
const slot = await gridService.getGridSlot('browser', 'executionId', { useLocalChromeDriver: true }, 'workerId');
|
|
189
187
|
expect(slot).to.eql({ mode: 'local' });
|
|
190
188
|
sinon.assert.notCalled(getGridByIdStub);
|
|
191
189
|
sinon.assert.notCalled(getGridByNameStub);
|
|
192
190
|
sinon.assert.notCalled(addItemToGridCacheStub);
|
|
193
|
-
sinon.assert.calledOnce(onGridSlot);
|
|
194
191
|
});
|
|
195
192
|
|
|
196
193
|
it('should not access server when using useChromeLauncher flag', async () => {
|
|
197
|
-
const slot = await gridService.getGridSlot('browser', 'executionId',
|
|
194
|
+
const slot = await gridService.getGridSlot('browser', 'executionId', { useChromeLauncher: true }, 'workerId');
|
|
198
195
|
expect(slot).to.eql({ mode: 'local' });
|
|
199
196
|
sinon.assert.notCalled(getGridByIdStub);
|
|
200
197
|
sinon.assert.notCalled(getGridByNameStub);
|
|
201
198
|
sinon.assert.notCalled(addItemToGridCacheStub);
|
|
202
|
-
sinon.assert.calledOnce(onGridSlot);
|
|
203
199
|
});
|
|
204
200
|
|
|
205
201
|
it('should return fixed grid when passing host and port', async () => {
|
|
206
|
-
const slot = await gridService.getGridSlot('browser', 'executionId',
|
|
202
|
+
const slot = await gridService.getGridSlot('browser', 'executionId', { host: 'localhost', port: 4444 }, 'workerId');
|
|
207
203
|
expect(slot).to.shallowDeepEqual({ type: 'hostAndPort', host: 'localhost', port: 4444 });
|
|
208
204
|
sinon.assert.notCalled(getGridByIdStub);
|
|
209
205
|
sinon.assert.notCalled(getGridByNameStub);
|
|
210
206
|
sinon.assert.notCalled(addItemToGridCacheStub);
|
|
211
|
-
sinon.assert.calledOnce(onGridSlot);
|
|
212
207
|
});
|
|
213
208
|
|
|
214
209
|
it('should get grid from server when passing grid id', async () => {
|
|
215
|
-
const slot = await gridService.getGridSlot('browser', 'executionId',
|
|
210
|
+
const slot = await gridService.getGridSlot('browser', 'executionId', { gridId: 'gridId' }, 'workerId');
|
|
216
211
|
expect(slot).to.shallowDeepEqual({ type: 'gridId', gridId: 'gridId' });
|
|
217
212
|
sinon.assert.calledOnce(getGridByIdStub);
|
|
218
213
|
sinon.assert.notCalled(getGridByNameStub);
|
|
219
214
|
sinon.assert.calledOnce(addItemToGridCacheStub);
|
|
220
|
-
sinon.assert.calledOnce(onGridSlot);
|
|
221
215
|
});
|
|
222
216
|
|
|
223
217
|
it('should get grid from server when passing grid name', async () => {
|
|
224
|
-
const slot = await gridService.getGridSlot('browser', 'executionId',
|
|
218
|
+
const slot = await gridService.getGridSlot('browser', 'executionId', { grid: 'gridName' }, 'workerId');
|
|
225
219
|
expect(slot).to.shallowDeepEqual({ type: 'gridName', gridId: 'gridId' });
|
|
226
220
|
sinon.assert.calledOnce(getGridByNameStub);
|
|
227
221
|
sinon.assert.notCalled(getGridByIdStub);
|
|
228
222
|
sinon.assert.calledOnce(addItemToGridCacheStub);
|
|
229
|
-
sinon.assert.calledOnce(onGridSlot);
|
|
230
223
|
});
|
|
231
224
|
|
|
232
225
|
it('should handle grid not found error', async () => {
|
|
233
226
|
getGridByIdStub.resolves({ status: 'error', code: 'not-found' });
|
|
234
|
-
await expect(gridService.getGridSlot('browser', 'executionId',
|
|
227
|
+
await expect(gridService.getGridSlot('browser', 'executionId', { gridId: 'gridId' }, 'workerId'))
|
|
235
228
|
.to.eventually.be.rejectedWith('The specified grid is not available');
|
|
236
229
|
});
|
|
237
230
|
|
|
238
231
|
it('should handle no available slot error', async () => {
|
|
239
232
|
getGridByIdStub.resolves({ status: 'error', code: 'no-available-slot' });
|
|
240
|
-
await expect(gridService.getGridSlot('browser', 'executionId',
|
|
233
|
+
await expect(gridService.getGridSlot('browser', 'executionId', { gridId: 'gridId' }, 'workerId'))
|
|
241
234
|
.to.eventually.be.rejectedWith('Failed to run test on browser - concurrency limit reached');
|
|
242
235
|
});
|
|
243
236
|
|
|
244
237
|
it('should handle getGridSlot request error', async () => {
|
|
245
238
|
getGridByIdStub.rejects({ status: 'error', code: 'not-found' });
|
|
246
|
-
await expect(gridService.getGridSlot('browser', 'executionId',
|
|
239
|
+
await expect(gridService.getGridSlot('browser', 'executionId', { gridId: 'gridId' }, 'workerId'))
|
|
247
240
|
.to.eventually.be.rejectedWith('Test couldn\'t get browser - unknown error');
|
|
248
241
|
});
|
|
249
242
|
|
|
250
243
|
it('should throw when no grid selected', async () => {
|
|
251
|
-
await expect(gridService.getGridSlot('browser', 'executionId',
|
|
244
|
+
await expect(gridService.getGridSlot('browser', 'executionId', {}, 'workerId'))
|
|
252
245
|
.to.eventually.be.rejectedWith('Missing host or grid configuration');
|
|
253
246
|
});
|
|
254
247
|
|
|
255
248
|
it('should handle unkonwn errors', async () => {
|
|
256
249
|
getGridByIdStub.resolves({ status: 'error', code: 'bla bla' });
|
|
257
|
-
await expect(gridService.getGridSlot('browser', 'executionId',
|
|
250
|
+
await expect(gridService.getGridSlot('browser', 'executionId', { gridId: 'gridId' }, 'workerId'))
|
|
258
251
|
.to.eventually.be.rejectedWith('Test couldn\'t get browser - unknown error');
|
|
259
252
|
});
|
|
260
253
|
|
|
261
254
|
it('should handle no status', async () => {
|
|
262
255
|
getGridByIdStub.resolves({ code: 'bla bla' });
|
|
263
|
-
await expect(gridService.getGridSlot('browser', 'executionId',
|
|
256
|
+
await expect(gridService.getGridSlot('browser', 'executionId', { gridId: 'gridId' }, 'workerId'))
|
|
264
257
|
.to.eventually.be.rejectedWith('Test couldn\'t get browser - unknown error');
|
|
265
258
|
});
|
|
266
259
|
});
|
package/testRunStatus.js
CHANGED
|
@@ -233,7 +233,11 @@ class RunStatus {
|
|
|
233
233
|
return this.testStartReport(test, executionId, testRetryKey);
|
|
234
234
|
}
|
|
235
235
|
|
|
236
|
-
|
|
236
|
+
/**
|
|
237
|
+
* @param {string} resultId
|
|
238
|
+
* @param {Awaited<ReturnType<typeof import('./services/gridService')['getGridSlot']>>} gridInfo
|
|
239
|
+
*/
|
|
240
|
+
onGridSlot(resultId, gridInfo) {
|
|
237
241
|
const test = this.getTestResult(resultId);
|
|
238
242
|
test.config.gridInfo = Object.assign({}, gridInfo, { key: undefined, user: undefined });
|
|
239
243
|
logger.info('on get grid info', { gridInfo: test.config.gridInfo });
|
|
@@ -264,6 +268,10 @@ class RunStatus {
|
|
|
264
268
|
return result.success ? constants.runnerTestStatus.PASSED : constants.runnerTestStatus.FAILED;
|
|
265
269
|
}
|
|
266
270
|
|
|
271
|
+
/**
|
|
272
|
+
* @param {number} wid
|
|
273
|
+
* @param {Parameters<ConstructorParameters<typeof import('./workers/BaseWorker')>[7]>[1]} resultId
|
|
274
|
+
*/
|
|
267
275
|
onTestIgnored(wid, resultId) {
|
|
268
276
|
const test = this.getTestResult(resultId);
|
|
269
277
|
reporter.onTestIgnored(wid, test, `test in ${constants.testStatus.QUARANTINE}`);
|
package/workers/BaseWorker.js
CHANGED
|
@@ -27,6 +27,12 @@ const { SETUP_TIMEOUT, NETWORK_ERROR, GRID_ERROR, BROWSER_CLOSED, SELENIUM_ERROR
|
|
|
27
27
|
const DELAY_BETWEEN_TESTS = ms('1s');
|
|
28
28
|
let ordinal = 1;
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* @param {string} testId
|
|
32
|
+
* @param {string} testName
|
|
33
|
+
* @param {string} resultId
|
|
34
|
+
* @param {string} reason
|
|
35
|
+
*/
|
|
30
36
|
function buildFailureResult(testId, testName, resultId, reason) {
|
|
31
37
|
return {
|
|
32
38
|
testId,
|
|
@@ -43,10 +49,10 @@ class BaseWorker {
|
|
|
43
49
|
* @param {import('../runOptions').RunnerOptions} options
|
|
44
50
|
* @param {string=} customExtensionLocalLocation
|
|
45
51
|
* @param {string} executionId
|
|
46
|
-
* @param {
|
|
47
|
-
* @param {
|
|
48
|
-
* @param {
|
|
49
|
-
* @param {
|
|
52
|
+
* @param {(workerId: number, testId: string, resultId: string, isRerun: boolean | undefined, testRetryKey: `${number}:${number}` ) => Promise<any>} onTestStarted
|
|
53
|
+
* @param {(workerId: number, testId: string, testResult: any, sessionId: string, shouldRerun: boolean | undefined) => Promise<void>} onTestCompleted
|
|
54
|
+
* @param {import('../testRunStatus')['onGridSlot']} onGridSlot
|
|
55
|
+
* @param {(workerId: number, testResult: { name: string; testId: string; resultId: string; runnerStatus: 'SKIPPED'; testStatus: 'quarantine' }) => void} onTestIgnored
|
|
50
56
|
* @param {boolean=} releaseSlotOnTestFinished
|
|
51
57
|
*/
|
|
52
58
|
constructor(executionQueue, options, customExtensionLocalLocation, executionId, onTestStarted, onTestCompleted, onGridSlot, onTestIgnored, releaseSlotOnTestFinished = true) {
|
|
@@ -75,10 +81,17 @@ class BaseWorker {
|
|
|
75
81
|
return ordinal++;
|
|
76
82
|
}
|
|
77
83
|
|
|
78
|
-
|
|
79
|
-
|
|
84
|
+
/**
|
|
85
|
+
* @param {string} browser
|
|
86
|
+
* @param {import('../testRunHandler')} testRunHandler
|
|
87
|
+
*/
|
|
88
|
+
async getGridSlot(browser, testRunHandler) {
|
|
89
|
+
const slot = await gridService.getGridSlot(browser, testRunHandler.getExecutionId(), this.options, this.id);
|
|
90
|
+
this.onGridSlot(testRunHandler.getTestResultId(), slot);
|
|
91
|
+
return slot;
|
|
80
92
|
}
|
|
81
93
|
|
|
94
|
+
/** @param {import('../testRunHandler')} testRunHandler */
|
|
82
95
|
async getSlotOnce(testRunHandler) {
|
|
83
96
|
const { browserValue } = this.testRunConfig;
|
|
84
97
|
reporter.onGetSlot(this.id, browserValue || 'chrome');
|
|
@@ -86,6 +99,10 @@ class BaseWorker {
|
|
|
86
99
|
return gridInfo;
|
|
87
100
|
}
|
|
88
101
|
|
|
102
|
+
/**
|
|
103
|
+
* @abstract
|
|
104
|
+
* @returns {import('../player/appiumTestPlayer') | import('../player/seleniumTestPlayer') | import('../player/extensionTestPlayer') | import('../player/chromeLauncherTestPlayer')}
|
|
105
|
+
*/
|
|
89
106
|
initPlayer() {
|
|
90
107
|
throw new NotImplementedError(true);
|
|
91
108
|
}
|
|
@@ -94,6 +111,10 @@ class BaseWorker {
|
|
|
94
111
|
throw new NotImplementedError(true);
|
|
95
112
|
}
|
|
96
113
|
|
|
114
|
+
/**
|
|
115
|
+
* @param {import('../testRunHandler')} testRunHandler
|
|
116
|
+
* @param {ReturnType<typeof this['initPlayer']>} player
|
|
117
|
+
*/
|
|
97
118
|
async runTestOnce(testRunHandler, player) {
|
|
98
119
|
testRunHandler.setSessionId(player.getSessionId());
|
|
99
120
|
logger.info('Test run started', {
|
|
@@ -105,19 +126,20 @@ class BaseWorker {
|
|
|
105
126
|
return await testRunHandler.clearTestResult();
|
|
106
127
|
}
|
|
107
128
|
|
|
129
|
+
/** @param {import('../testRunHandler')} testRunHandler */
|
|
108
130
|
handleQuarantine(testRunHandler) {
|
|
109
|
-
if (utils.isQuarantineAndNotRemoteRun({ testStatus: testRunHandler.getTestStatus() }, this.options)) {
|
|
110
|
-
|
|
111
|
-
name: testRunHandler.getTestName(),
|
|
112
|
-
testId: testRunHandler.getTestId(),
|
|
113
|
-
resultId: testRunHandler.getTestResultId(),
|
|
114
|
-
runnerStatus: runnerTestStatus.SKIPPED,
|
|
115
|
-
testStatus: testRunHandler.getTestStatus(),
|
|
116
|
-
};
|
|
117
|
-
this.onTestIgnored(this.id, testResult);
|
|
118
|
-
return testResult;
|
|
131
|
+
if (!utils.isQuarantineAndNotRemoteRun({ testStatus: testRunHandler.getTestStatus() }, this.options)) {
|
|
132
|
+
return undefined;
|
|
119
133
|
}
|
|
120
|
-
|
|
134
|
+
const testResult = {
|
|
135
|
+
name: testRunHandler.getTestName(),
|
|
136
|
+
testId: testRunHandler.getTestId(),
|
|
137
|
+
resultId: testRunHandler.getTestResultId(),
|
|
138
|
+
runnerStatus: runnerTestStatus.SKIPPED,
|
|
139
|
+
testStatus: testRunHandler.getTestStatus(),
|
|
140
|
+
};
|
|
141
|
+
this.onTestIgnored(this.id, testResult);
|
|
142
|
+
return testResult;
|
|
121
143
|
}
|
|
122
144
|
|
|
123
145
|
setSessionTimeout() {
|
|
@@ -127,6 +149,10 @@ class BaseWorker {
|
|
|
127
149
|
return Math.max(this.lambdatestService.getSessionTimeout, this.options.getSessionTimeout);
|
|
128
150
|
}
|
|
129
151
|
|
|
152
|
+
/**
|
|
153
|
+
* @param {import('../testRunHandler')} testRunHandler
|
|
154
|
+
* @param {string=} customExtensionLocalLocation
|
|
155
|
+
*/
|
|
130
156
|
async getTestPlayer(testRunHandler, customExtensionLocalLocation) {
|
|
131
157
|
const projectId = this.userData?.projectId;
|
|
132
158
|
let testPlayer;
|
|
@@ -202,6 +228,11 @@ class BaseWorker {
|
|
|
202
228
|
return testPlayer;
|
|
203
229
|
}
|
|
204
230
|
|
|
231
|
+
/**
|
|
232
|
+
* @param {import('../testRunHandler')} testRunHandler
|
|
233
|
+
* @param {string=} customExtensionLocalLocation
|
|
234
|
+
* @param {boolean=} shouldRerun
|
|
235
|
+
*/
|
|
205
236
|
async runTest(testRunHandler, customExtensionLocalLocation, shouldRerun) {
|
|
206
237
|
perf.log('inside runTest');
|
|
207
238
|
const projectId = this.userData?.projectId;
|
|
@@ -233,6 +264,11 @@ class BaseWorker {
|
|
|
233
264
|
run() {
|
|
234
265
|
const runNextTest = () => process.nextTick(() => this.run());
|
|
235
266
|
|
|
267
|
+
/**
|
|
268
|
+
* @param {*} testResult
|
|
269
|
+
* @param {import('../testRunHandler')} testRunHandler
|
|
270
|
+
* @param {Error=} err
|
|
271
|
+
*/
|
|
236
272
|
const onRunComplete = async (testResult, testRunHandler, err) => {
|
|
237
273
|
if (utils.isQuarantineAndNotRemoteRun(testResult, this.options)) {
|
|
238
274
|
return runNextTest();
|
|
@@ -287,6 +323,10 @@ class BaseWorker {
|
|
|
287
323
|
const getNetworkErrorMessage = () => 'Due to network connectivity issues, Testim CLI has been unable to connect to the grid.\n' +
|
|
288
324
|
`Please make sure the CLI has stable access to the internet. ${didNetworkConnectivityTestFail() ? '(Internal: network connectivity test failed)' : ''}`;
|
|
289
325
|
|
|
326
|
+
/**
|
|
327
|
+
* @param {Error} err
|
|
328
|
+
* @param {boolean} wasNetworkHealthy
|
|
329
|
+
*/
|
|
290
330
|
const buildError = (err, wasNetworkHealthy) => {
|
|
291
331
|
if (!wasNetworkHealthy && featureFlags.flags.errorMessageOnBadNetwork.isEnabled()) {
|
|
292
332
|
return {
|
|
@@ -344,6 +384,10 @@ class BaseWorker {
|
|
|
344
384
|
return { errorType: UNKNOWN_ERROR, reason: msg };
|
|
345
385
|
};
|
|
346
386
|
|
|
387
|
+
/**
|
|
388
|
+
* @param {Error} err
|
|
389
|
+
* @param {import('../testRunHandler')} testRunHandler
|
|
390
|
+
*/
|
|
347
391
|
const onRunError = async (err, testRunHandler) => {
|
|
348
392
|
const wasNetworkHealthy = await isNetworkHealthy();
|
|
349
393
|
if (!wasNetworkHealthy && featureFlags.flags.warnOnBadNetwork.isEnabled()) {
|
|
@@ -366,6 +410,10 @@ class BaseWorker {
|
|
|
366
410
|
await onRunComplete(buildFailureResult(this.testId, this.testName, this.testResultId, reason), testRunHandler, err);
|
|
367
411
|
};
|
|
368
412
|
|
|
413
|
+
/**
|
|
414
|
+
* @param {Error} runError
|
|
415
|
+
* @param {import('../testRunHandler')} testRunHandler
|
|
416
|
+
*/
|
|
369
417
|
const recoverTestResults = async (runError, testRunHandler) => {
|
|
370
418
|
const testId = this.testId;
|
|
371
419
|
const resultId = this.testResultId;
|
|
@@ -401,23 +449,29 @@ class BaseWorker {
|
|
|
401
449
|
const disableResults = this.options.disableSockets || (this.options.lightweightMode?.disableResults && (this.options.useChromeLauncher || this.options.mode !== 'extension'));
|
|
402
450
|
const disableRemoteStep = this.options.disableSockets || (this.options.lightweightMode?.disableRemoteStep);
|
|
403
451
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
452
|
+
/**
|
|
453
|
+
* @param {import('../testRunHandler')} testRunHandler
|
|
454
|
+
* @param {boolean=} shouldRerun
|
|
455
|
+
*/
|
|
456
|
+
const runTestAndCalcResult = async (testRunHandler, shouldRerun) => {
|
|
457
|
+
try {
|
|
458
|
+
await Promise.all([
|
|
459
|
+
!disableRemoteStep && remoteStepService.joinToRemoteStep(this.testResultId),
|
|
460
|
+
!disableResults && testResultService.joinToTestResult(this.testResultId, this.testId),
|
|
461
|
+
]);
|
|
462
|
+
testRunHandler.validateRunConfig();
|
|
463
|
+
const testResult = await this.runTest(testRunHandler, this.customExtensionLocalLocation, shouldRerun);
|
|
464
|
+
const result = await onRunComplete(testResult, testRunHandler);
|
|
412
465
|
perf.log('After onRunComplete');
|
|
413
466
|
return result;
|
|
414
|
-
})
|
|
415
|
-
|
|
416
|
-
|
|
467
|
+
} catch (runError) {
|
|
468
|
+
return recoverTestResults(runError, testRunHandler);
|
|
469
|
+
} finally {
|
|
417
470
|
if (!disableRemoteStep) {
|
|
418
471
|
remoteStepService.unlistenToRemoteStep(this.testResultId);
|
|
419
472
|
}
|
|
420
|
-
}
|
|
473
|
+
}
|
|
474
|
+
};
|
|
421
475
|
|
|
422
476
|
const testRunHandler = this.executionQueue.getNext();
|
|
423
477
|
|
|
@@ -25,7 +25,7 @@ describe('BaseWorker', () => {
|
|
|
25
25
|
},
|
|
26
26
|
});
|
|
27
27
|
|
|
28
|
-
worker = new BaseWorker(null, {}, null, null, onTestStartedStub);
|
|
28
|
+
worker = new BaseWorker(null, {}, null, null, onTestStartedStub, null, () => { /* noop */ });
|
|
29
29
|
worker.userData = {};
|
|
30
30
|
worker.options = { gridData: {}, browser: 'chrome', company: { companyId: 'companyId' }, getBrowserTimeout: 1000, getSessionTimeout: 100, getBrowserRetries: 10 };
|
|
31
31
|
worker.testRunConfig = {};
|
package/workers/WorkerAppium.js
CHANGED
|
@@ -10,11 +10,6 @@ const AppiumApi = require('../commons/getSessionPlayerRequire').AppiumApi;
|
|
|
10
10
|
const desiredCapabilitiesBuilder = require('../commons/testimDesiredCapabilitiesBuilder');
|
|
11
11
|
|
|
12
12
|
class WorkerAppium extends BaseWorker {
|
|
13
|
-
constructor(...args) {
|
|
14
|
-
super(...args);
|
|
15
|
-
this.getBrowserOnce = Promise.method(this.getBrowserOnce);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
13
|
initPlayer(testRunHandler) {
|
|
19
14
|
return new AppiumTestPlayer(this.id,
|
|
20
15
|
testRunHandler.getRunParams(),
|
|
@@ -27,6 +27,11 @@ class WorkerExtensionSingleBrowser extends WorkerExtension {
|
|
|
27
27
|
return this._getBrowserOnce(testRunHandler, customExtensionLocalLocation, player, gridInfo);
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* @override
|
|
32
|
+
* @param {import('../testRunHandler')} testRunHandler
|
|
33
|
+
* @param {string=} customExtensionLocalLocation
|
|
34
|
+
*/
|
|
30
35
|
async getTestPlayer(testRunHandler, customExtensionLocalLocation) {
|
|
31
36
|
if (this.testPlayer && !this.testPlayer.driver.isAlive()) {
|
|
32
37
|
logger.warn('WorkerExtensionSingleBrowser is releasing a dead player', { workerId: this.id });
|
|
@@ -17,21 +17,29 @@ const { preloadTests } = require('../commons/preloadTests');
|
|
|
17
17
|
const NO_NAVIGATION_TIME_LIMIT = 1e9;
|
|
18
18
|
|
|
19
19
|
class WorkerSelenium extends BaseWorker {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
/**
|
|
21
|
+
* @override
|
|
22
|
+
* @param {import('../testRunHandler')} testRunHandler
|
|
23
|
+
*/
|
|
25
24
|
initPlayer(testRunHandler) {
|
|
26
|
-
return new SeleniumTestPlayer(
|
|
25
|
+
return new SeleniumTestPlayer(
|
|
26
|
+
this.id,
|
|
27
27
|
testRunHandler.getRunParams(),
|
|
28
28
|
this.options.shouldMonitorPerformance,
|
|
29
29
|
testRunHandler.getAutomationMode(),
|
|
30
30
|
undefined,
|
|
31
31
|
testRunHandler.getRetryCount(),
|
|
32
|
-
testRunHandler.getPreviousTestResultId()
|
|
32
|
+
testRunHandler.getPreviousTestResultId(),
|
|
33
|
+
);
|
|
33
34
|
}
|
|
34
35
|
|
|
36
|
+
/**
|
|
37
|
+
* @override
|
|
38
|
+
* @param {import('../testRunHandler')} testRunHandler
|
|
39
|
+
* @param {string=} customExtensionLocalLocation
|
|
40
|
+
* @param {SeleniumTestPlayer} seleniumTestPlayer
|
|
41
|
+
* @param {Awaited<ReturnType<import('../services/gridService')['handleHybridOrVendorIfNeeded']>>} gridInfo
|
|
42
|
+
*/
|
|
35
43
|
async getBrowserOnce(testRunHandler, customExtensionLocalLocation, seleniumTestPlayer, gridInfo) {
|
|
36
44
|
perf.log('in WorkerSelenium getBrowserOnce');
|
|
37
45
|
reporter.onGetSession(this.id, this.testName, testRunHandler.getRunMode());
|
|
@@ -81,6 +89,7 @@ class WorkerSelenium extends BaseWorker {
|
|
|
81
89
|
}
|
|
82
90
|
|
|
83
91
|
/**
|
|
92
|
+
* @override
|
|
84
93
|
* @param {import('../testRunHandler')} testRunHandler
|
|
85
94
|
* @param {import('../player/seleniumTestPlayer')} seleniumTestPlayer
|
|
86
95
|
*/
|