UNPKG

12.6 kBJavaScriptView Raw
1'use strict';
2
3const Promise = require('bluebird');
4const moment = require('moment');
5
6const { timeoutMessages, testStatus } = require('../commons/constants');
7const logger = require('../commons/logger').getLogger('base-worker');
8const testResultService = require('../commons/socket/testResultService');
9const remoteStepService = require('../commons/socket/remoteStepService');
10const { isNetworkHealthy } = require('../commons/httpRequest');
11const testimServicesApi = require('../commons/testimServicesApi');
12const gridService = require('../services/gridService');
13const reporter = require('../reports/reporter');
14const utils = require('../utils');
15const { getBrowserWithRetries, isGetBrowserError, releasePlayer } = require('./workerUtils');
16const featureFlags = require('../commons/featureFlags');
17
18const { GET_BROWSER_TIMEOUT_MSG, TEST_START_TIMEOUT_MSG, TEST_COMPLETE_TIMEOUT_MSG } = timeoutMessages;
19
20const {
21 SeleniumError, StopRunOnError, GridError, GetBrowserError,
22} = require('../errors');
23
24const DELAY_BETWEEN_TESTS = 1000;
25let ordinal = 1;
26
27function buildFailureResult(testId, testName, resultId, err) {
28 return {
29 testId,
30 reason: err,
31 name: testName,
32 resultId,
33 success: false,
34 };
35}
36
37class BaseWorker {
38 constructor(executionQueue, customExtensionLocalLocation) {
39 this.id = BaseWorker.getWorkerId();
40 this.executionQueue = executionQueue;
41 this.customExtensionLocalLocation = customExtensionLocalLocation;
42 }
43
44 static getWorkerId() {
45 return ordinal++;
46 }
47
48 getBrowserOnce() {
49 throw new Error('not implemented');
50 }
51
52 initPlayer() {
53 throw new Error('not implemented');
54 }
55
56 runTestOnce(testRunHandler, player) {
57 testRunHandler.setSessionId(player.getSessionId());
58 return testRunHandler.clearTestResult();
59 }
60
61 getGridSlot(browser, testRunHandler) {
62 return gridService.getGridSlot(browser, testRunHandler.getExecutionId(), testRunHandler.getTestResultId(), this.onGridSlot, this.options, this.id);
63 }
64
65 start(options, onStart, onResult, executionId, onGridSlot, releaseSlotOnTestFinished) {
66 this.isCodeMode = options.files.length > 0;
67 this.baseUrl = options.baseUrl;
68 this.isRegressionBaselineRun = options.isRegressionBaselineRun;
69 this.testRunTimeout = options.timeout;
70 this.getBrowserTimeout = options.browserTimeout;
71 this.newBrowserWaitTimeout = options.newBrowserWaitTimeout;
72 this.onStart = onStart;
73 this.onResult = onResult;
74 this.onGridSlot = onGridSlot;
75 this.releaseSlotOnTestFinished = releaseSlotOnTestFinished;
76
77 this.userData = options.userData;
78 this.executionId = executionId;
79 this.options = options;
80 return this.run();
81 }
82
83 runTest(testRunHandler, customExtensionLocalLocation, shouldRerun) {
84 const WAIT_BETWEEN_INTERVALS = 3000;
85 const retryCount = this.options.disableTimeoutRetry ? 1 : 3;
86 const projectId = this.userData && this.userData.projectId;
87
88 return this.onStart(this.id, testRunHandler.getTestId(), testRunHandler.getTestResultId(), shouldRerun)
89 .then(() => utils.runWithRetries(() => {
90 let localPlayer;
91 const callbacksForBrowserRetryLogic = {
92 getBrowserOnce: this.getBrowserOnce.bind(this, testRunHandler, customExtensionLocalLocation),
93 testPlayerFactory: this.initPlayer.bind(this, testRunHandler),
94 releaseSlotOnTestFinished: this.releaseSlotOnTestFinished,
95 };
96 const browserFetchConfig = {
97 totalTimeoutDuration: this.newBrowserWaitTimeout,
98 singleGetBrowserDuration: this.getBrowserTimeout,
99 projectId,
100 workerId: this.id,
101 reporter,
102 };
103 const logger = require('../commons/performance-logger');
104 logger.log('before getBrowserWithRetries');
105 return getBrowserWithRetries(callbacksForBrowserRetryLogic, browserFetchConfig).then((player) => {
106 logger.log('after getBrowserWithRetries');
107 localPlayer = player;
108 return this.runTestOnce(testRunHandler, player);
109 }).finally(() => releasePlayer(this.id, this.releaseSlotOnTestFinished, projectId, localPlayer));
110 }, retryCount, WAIT_BETWEEN_INTERVALS, err => !isGetBrowserError(err)));
111 }
112
113 runTestCleanup() {
114 return Promise.resolve();
115 }
116
117 run() {
118 const runNextTest = () => process.nextTick(() => this.run());
119
120 const onRunComplete = (testResult, testRunHandler, err) => {
121 const sessionId = testRunHandler.getSessionId();
122
123 const isTimeoutError = (timeoutMsg) => err.message.includes(timeoutMsg);
124 const isIgnoreErrors = err && (err instanceof GetBrowserError || isTimeoutError(TEST_START_TIMEOUT_MSG) || isTimeoutError(TEST_COMPLETE_TIMEOUT_MSG));
125 const shouldRerun = !testResult.success && testRunHandler.hasMoreRetries() && !isIgnoreErrors;
126
127 return this.onResult(this.id, this.testId, testResult, sessionId, shouldRerun)
128 .delay(DELAY_BETWEEN_TESTS)
129 .then(() => this.runTestCleanup())
130 .then(() => {
131 if (shouldRerun) {
132 testRunHandler.startNewRetry();
133 logger.info(`retry test id: ${this.testId} name: ${this.testName} again`, {
134 testId: this.testId,
135 testName: this.testName,
136 });
137 return runTestAndCalcResult(testRunHandler, shouldRerun);
138 }
139 return runNextTest();
140 })
141 .catch(err => {
142 if (err instanceof StopRunOnError) {
143 return;
144 }
145 logger.error('failed to process test result', { err });
146 runNextTest();
147 });
148 };
149
150 const buildErrorMsg = (err) => {
151 const isError = err instanceof Error || err instanceof GridError;
152 const msg = isError ? err.message : err;
153 if (!isNetworkHealthy() && featureFlags.flags.errorMessageOnBadNetwork.isEnabled()) {
154 return 'Due to network connectivity issues, Testim CLI has been unable to connect to the grid.' +
155 'Please make sure the CLI has stable access to the internet.';
156 }
157 if (msg.indexOf(GET_BROWSER_TIMEOUT_MSG) > -1) {
158 return "Test couldn't get browser";
159 } if (msg.indexOf(TEST_START_TIMEOUT_MSG) > -1) {
160 return "Test couldn't be started";
161 } if (msg.indexOf(TEST_COMPLETE_TIMEOUT_MSG) > -1) {
162 if (!this.testRunTimeout) {
163 return 'Test timeout reached: test is too long';
164 }
165
166 const duration = moment.duration(this.testRunTimeout, 'milliseconds');
167 const minutesCount = Math.floor(duration.asMinutes());
168 const secondsCount = duration.seconds();
169 const minutesTimeoutStr = minutesCount > 0 ? ` ${minutesCount} min` : '';
170 const secondsTimoutStr = secondsCount > 0 ? ` ${secondsCount} sec` : '';
171
172 return `Test timeout reached (timeout:${minutesTimeoutStr}${secondsTimoutStr}): test is too long`;
173 } if (err.failure && err.failure instanceof SeleniumError) {
174 return `Test couldn't get browser from grid - ${err.failure.message}`;
175 } if (/SeleniumError: connect ECONNREFUSED/.test(err.message)) {
176 return 'Failed to connect to the grid, please check if the grid is accessible from your network';
177 } if (/terminated due to FORWARDING_TO_NODE_FAILED/.test(err.message)) {
178 return 'Session terminated, it is likely that the grid is out of memory or not responding, please try to rerun the test';
179 } if (/terminated due to PROXY_REREGISTRATION/.test(err.message)) {
180 return 'Session terminated, it is likely that the grid is not responding, please try to rerun the test';
181 } if (/forwarding the new session cannot find : Capabilities/.test(err.message)) {
182 return 'Session could not be created, please check that the browser you requested is supported in your plan';
183 }
184 return msg;
185 };
186
187 const onRunError = (err, testRunHandler) => {
188 const failureReason = buildErrorMsg(err);
189 if (!isNetworkHealthy() && featureFlags.flags.warnOnBadNetwork.isEnabled()) {
190 // intentional, we want to log to stderr:
191 // eslint-disable-next-line no-console
192 console.warn('Due to network connectivity issues, Testim CLI has been unable to connect to the grid.');
193 // eslint-disable-next-line no-console
194 console.warn('Please make sure the CLI has stable access to the internet.');
195 }
196 logger.warn('error on run', { err });
197 const projectId = this.userData && this.userData.projectId;
198 return testResultService.updateTestResult(projectId, this.testResultId, this.testId, {
199 status: testStatus.COMPLETED,
200 success: false,
201 reason: failureReason,
202 setupStepResult: { status: testStatus.COMPLETED, success: false, reason: failureReason },
203 }, testRunHandler.getRemoteRunId()).then(() => onRunComplete(buildFailureResult(this.testId, this.testName, this.testResultId, failureReason), testRunHandler, err));
204 };
205
206 const recoverTestResults = (runError, testRunHandler) => {
207 const testId = this.testId;
208 const resultId = this.testResultId;
209 const projectId = this.userData && this.userData.projectId;
210 const branch = this.branch;
211 if (testId && resultId && projectId && branch) {
212 return testimServicesApi.getTestResults(testId, resultId, projectId, branch)
213 .then(testResult => {
214 if (testResult && testResult.status === testStatus.COMPLETED) {
215 logger.warn('Test failed. Got results via API', { err: runError });
216 return onRunComplete(testResult, testRunHandler);
217 }
218 return Promise.reject(runError);
219 })
220 .catch(err => {
221 if (err !== runError) {
222 logger.error('Failed to fetch test results from server', {
223 testId,
224 resultId,
225 projectId,
226 branch,
227 err,
228 });
229 }
230 return onRunError(runError, testRunHandler);
231 });
232 }
233
234 // Not enough data to call the API
235 return onRunError(runError, testRunHandler);
236 };
237
238 const runTestAndCalcResult = (testRunHandler, shouldRerun) => Promise.all([
239 remoteStepService.joinToRemoteStep(this.testResultId),
240 testResultService.joinToTestResult(this.testResultId, this.testId),
241 ])
242 .log('after join room, before runTest')
243 .then(() => this.runTest(testRunHandler, this.customExtensionLocalLocation, shouldRerun))
244 .then(testResult => onRunComplete(testResult, testRunHandler))
245 .log('After onRunComplete')
246 .catch(runError => recoverTestResults(runError, testRunHandler))
247 .finally(() => remoteStepService.unlistenToRemoteStep(this.testResultId));
248
249 const testRunHandler = this.executionQueue.getNext();
250 if (!testRunHandler) { // no more tests to run
251 return undefined;
252 }
253 this.testId = testRunHandler.getTestId();
254 this.testName = testRunHandler.getTestName();
255 this.testResultId = testRunHandler.getTestResultId();
256 this.overrideTestConfigId = testRunHandler.getOverrideTestConfigId();
257 this.testRunConfig = testRunHandler.getRunConfig();
258 this.branch = testRunHandler.getBranch();
259 return runTestAndCalcResult(testRunHandler);
260 }
261}
262
263module.exports = BaseWorker;