1 | 'use strict';
|
2 |
|
3 | const Promise = require('bluebird');
|
4 | const moment = require('moment');
|
5 |
|
6 | const { timeoutMessages, testStatus } = require('../commons/constants');
|
7 | const logger = require('../commons/logger').getLogger('base-worker');
|
8 | const testResultService = require('../commons/socket/testResultService');
|
9 | const remoteStepService = require('../commons/socket/remoteStepService');
|
10 | const { isNetworkHealthy } = require('../commons/httpRequest');
|
11 | const testimServicesApi = require('../commons/testimServicesApi');
|
12 | const gridService = require('../services/gridService');
|
13 | const reporter = require('../reports/reporter');
|
14 | const utils = require('../utils');
|
15 | const { getBrowserWithRetries, isGetBrowserError, releasePlayer } = require('./workerUtils');
|
16 | const featureFlags = require('../commons/featureFlags');
|
17 |
|
18 | const { GET_BROWSER_TIMEOUT_MSG, TEST_START_TIMEOUT_MSG, TEST_COMPLETE_TIMEOUT_MSG } = timeoutMessages;
|
19 |
|
20 | const {
|
21 | SeleniumError, StopRunOnError, GridError, GetBrowserError,
|
22 | } = require('../errors');
|
23 |
|
24 | const DELAY_BETWEEN_TESTS = 1000;
|
25 | let ordinal = 1;
|
26 |
|
27 | function buildFailureResult(testId, testName, resultId, err) {
|
28 | return {
|
29 | testId,
|
30 | reason: err,
|
31 | name: testName,
|
32 | resultId,
|
33 | success: false,
|
34 | };
|
35 | }
|
36 |
|
37 | class 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 |
|
191 |
|
192 | console.warn('Due to network connectivity issues, Testim CLI has been unable to connect to the grid.');
|
193 |
|
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 |
|
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) {
|
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 |
|
263 | module.exports = BaseWorker;
|