Skip to content

Commit a13a494

Browse files
authored
Merge pull request #91 from browserstack/sync-cli-retry-polling
[Sync CLI] Retry polling in case of network issue
2 parents 8b75f38 + 7b94fc3 commit a13a494

File tree

10 files changed

+155
-21
lines changed

10 files changed

+155
-21
lines changed

bin/commands/runs.js

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,7 @@ const archiver = require("../helpers/archiver"),
88
Constants = require("../helpers/constants"),
99
utils = require("../helpers/utils"),
1010
fileHelpers = require("../helpers/fileHelpers"),
11-
syncRunner = require("../helpers/syncRunner"),
12-
syncCliLogger = require("../helpers/logger").syncCliLogger;
11+
syncRunner = require("../helpers/syncRunner");
1312

1413
module.exports = function run(args) {
1514
let bsConfigPath = utils.getConfigPath(args.cf);
@@ -73,9 +72,7 @@ module.exports = function run(args) {
7372
if (args.sync) {
7473
syncRunner.pollBuildStatus(bsConfig, data).then((exitCode) => {
7574
utils.sendUsageReport(bsConfig, args, `${message}\n${dashboardLink}`, Constants.messageTypes.SUCCESS, null);
76-
syncCliLogger.info(Constants.userMessages.BUILD_REPORT_MESSAGE);
77-
syncCliLogger.info(data.dashboard_url);
78-
process.exit(exitCode);
75+
utils.handleSyncExit(exitCode, data.dashboard_url)
7976
});
8077
}
8178

bin/helpers/config.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,7 @@ config.cypress_v1 = `${config.rails_host}/automate/cypress/v1`;
1616
config.buildUrl = `${config.cypress_v1}/builds/`;
1717
config.buildStopUrl = `${config.cypress_v1}/builds/stop/`;
1818
config.fileName = "tests.zip";
19+
config.retries = 5;
20+
config.networkErrorExitCode = 2;
1921

2022
module.exports = config;

bin/helpers/constants.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
let config = require("./config");
2+
13
const syncCLI = {
24
FAILED_SPEC_DETAILS_COL_HEADER: ['Spec', 'Status', 'Browser', 'BrowserStack Session ID'],
35
LOGS: {
@@ -30,7 +32,10 @@ const userMessages = {
3032
UPLOADING_TESTS: "Uploading the tests to BrowserStack",
3133
LOCAL_TRUE: "you will now be able to test localhost / private URLs",
3234
LOCAL_FALSE: "you won't be able to test localhost / private URLs",
33-
EXIT_SYNC_CLI_MESSAGE: "Exiting the CLI, but your build is still running. You can use the --sync option to keep getting test updates. You can also use the build-info <build-id> command now."
35+
EXIT_SYNC_CLI_MESSAGE: "Exiting the CLI, but your build is still running. You can use the --sync option to keep getting test updates. You can also use the build-info <build-id> command now.",
36+
FATAL_NETWORK_ERROR: `fatal: unable to access '${config.buildUrl}': Could not resolve host: ${config.rails_host}`,
37+
RETRY_LIMIT_EXCEEDED: `Max retries exceeded trying to connect to the host (retries: ${config.retries})`,
38+
CHECK_DASHBOARD_AT: "Please check the build status at: "
3439
};
3540

3641
const validationMessages = {

bin/helpers/sync/failedSpecsDetails.js

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
const tablePrinter = require('table'), // { table, getBorderCharacters }
22
chalk = require('chalk'),
33
Constants = require("../constants"),
4-
logger = require("../logger").syncCliLogger;
4+
logger = require("../logger").syncCliLogger,
5+
config = require("../config");
56

67
/**
78
*
@@ -19,7 +20,8 @@ const tablePrinter = require('table'), // { table, getBorderCharacters }
1920
//
2021
let failedSpecsDetails = (data) => {
2122
return new Promise((resolve, reject) => {
22-
data.exitCode = 0;
23+
if (!data.exitCode) data.exitCode = 0;
24+
2325
if (data.specs.length === 0) resolve(data); // return if no failed/skipped tests.
2426

2527
let failedSpecs = false;
@@ -48,7 +50,7 @@ let failedSpecsDetails = (data) => {
4850
]);
4951
});
5052

51-
let config = {
53+
let tableConfig = {
5254
border: tablePrinter.getBorderCharacters('ramac'),
5355
columns: {
5456
0: { alignment: 'left' },
@@ -67,12 +69,12 @@ let failedSpecsDetails = (data) => {
6769
}
6870
}
6971

70-
let result = tablePrinter.table(specData, config);
72+
let result = tablePrinter.table(specData, tableConfig);
7173

7274
logger.info('\nFailed / skipped test report:');
7375
logger.info(result);
7476

75-
if (failedSpecs) data.exitCode = 1 ; // specs failed, send exitCode as 1
77+
if (failedSpecs && data.exitCode !== config.networkErrorExitCode) data.exitCode = 1 ; // specs failed, send exitCode as 1
7678
resolve(data); // No Specs failed, maybe skipped, but not failed, send exitCode as 0
7779
});
7880
}

bin/helpers/sync/syncSpecsLogs.js

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ const request = require("request"),
88
tableStream = require('table').createStream,
99
chalk = require('chalk');
1010

11-
let whileLoop = true, whileTries = 40, options, timeout = 3000, n = 10, tableConfig, stream, startTime, endTime;
11+
let whileLoop = true, whileTries = config.retries, options, timeout = 3000, n = 10, tableConfig, stream, endTime, startTime = Date.now();
1212
let specSummary = {
1313
"specs": [],
1414
"duration": null
@@ -22,6 +22,7 @@ let getOptions = (auth, build_id) => {
2222
user: auth.username,
2323
password: auth.access_key
2424
},
25+
timeout: 10000,
2526
headers: {
2627
"Content-Type": "application/json",
2728
"User-Agent": utils.getUserAgent()
@@ -80,6 +81,7 @@ let printSpecsStatus = (bsConfig, buildDetails) => {
8081
whileProcess(callback)
8182
},
8283
function(err, result) { // when loop ends
84+
logger.info("\n--------------------------------------------------------------------------------")
8385
specSummary.duration = endTime - startTime
8486
resolve(specSummary)
8587
}
@@ -90,8 +92,20 @@ let printSpecsStatus = (bsConfig, buildDetails) => {
9092
let whileProcess = (whilstCallback) => {
9193
request.post(options, function(error, response, body) {
9294
if (error) {
93-
return whilstCallback(error);
95+
whileTries -= 1;
96+
if (whileTries === 0) {
97+
whileLoop = false;
98+
endTime = Date.now();
99+
specSummary.exitCode = config.networkErrorExitCode;
100+
return whilstCallback({ status: 504, message: "Tries limit reached" }); //Gateway Timeout
101+
} else {
102+
n = 2
103+
return setTimeout(whilstCallback, timeout * n, null);
104+
}
94105
}
106+
107+
whileTries = config.retries; // reset to default after every successful request
108+
95109
switch (response.statusCode) {
96110
case 202: // get data here and print it
97111
n = 2
@@ -104,7 +118,6 @@ let whileProcess = (whilstCallback) => {
104118
whileLoop = false;
105119
endTime = Date.now();
106120
showSpecsStatus(body);
107-
logger.info("\n--------------------------------------------------------------------------------")
108121
return whilstCallback(null, body);
109122
default:
110123
whileLoop = false;
@@ -125,7 +138,6 @@ let showSpecsStatus = (data) => {
125138
}
126139

127140
let printInitialLog = () => {
128-
startTime = Date.now();
129141
logger.info(`\n${Constants.syncCLI.LOGS.INIT_LOG}`)
130142
logger.info("--------------------------------------------------------------------------------")
131143
n = Constants.syncCLI.INITIAL_DELAY_MULTIPLIER

bin/helpers/utils.js

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@ const fs = require("fs");
55

66
const usageReporting = require("./usageReporting"),
77
logger = require("./logger").winstonLogger,
8-
Constants = require("./constants");
8+
Constants = require("./constants"),
9+
chalk = require('chalk'),
10+
syncCliLogger = require("../helpers/logger").syncCliLogger,
11+
config = require("../helpers/config");
912

1013
exports.validateBstackJson = (bsConfigPath) => {
1114
return new Promise(function (resolve, reject) {
@@ -318,3 +321,20 @@ exports.setLocalIdentifier = (bsConfig) => {
318321
exports.capitalizeFirstLetter = (stringToCapitalize) => {
319322
return stringToCapitalize && (stringToCapitalize[0].toUpperCase() + stringToCapitalize.slice(1));
320323
};
324+
325+
exports.handleSyncExit = (exitCode, dashboard_url) => {
326+
if (exitCode === config.networkErrorExitCode) {
327+
syncCliLogger.info(this.getNetworkErrorMessage(dashboard_url));
328+
} else {
329+
syncCliLogger.info(Constants.userMessages.BUILD_REPORT_MESSAGE);
330+
syncCliLogger.info(dashboard_url);
331+
}
332+
process.exit(exitCode);
333+
}
334+
335+
exports.getNetworkErrorMessage = (dashboard_url) => {
336+
let message = Constants.userMessages.FATAL_NETWORK_ERROR + '\n'
337+
+ Constants.userMessages.RETRY_LIMIT_EXCEEDED + '\n'
338+
+ Constants.userMessages.CHECK_DASHBOARD_AT + dashboard_url
339+
return chalk.red(message)
340+
}

test/unit/bin/helpers/sync/failedSpecDetails.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,4 +59,18 @@ describe("failedSpecsDetails", () => {
5959
});
6060
});
6161
});
62+
63+
context("failed because of network issue", () => {
64+
let data = {
65+
specs: [ {specName: 'spec2.name.js', status: 'Failed', combination: 'Win 10 / Chrome 78', sessionId: '3d3rdf3r...'},
66+
{specName: 'spec2.name.js', status: 'Passed', combination: 'Win 10 / Chrome 78', sessionId: '3d3rdf3r...'}],
67+
exitCode: 2
68+
};
69+
70+
it("returns 2 exit code", () => {
71+
return specDetails.failedSpecsDetails(data).then((result) => {
72+
expect(result).to.equal(data);
73+
});
74+
});
75+
});
6276
});

test/unit/bin/helpers/sync/specSummary.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,15 @@ describe("printSpecsRunSummary", () => {
1818
});
1919
});
2020

21+
context("request failure", () => {
22+
let data = { specs: [], duration: 6000, exitCode: 2}, machines = 2;
23+
it('returns passed specs data with proper exit code', () => {
24+
return specSummary.printSpecsRunSummary(data, machines).then((specsData) => {
25+
expect(data.exitCode).to.equal(specsData);
26+
});
27+
});
28+
});
29+
2130
context("with data", () => {
2231
let time = 6000,
2332
machines = 2,

test/unit/bin/helpers/sync/syncSpecsLogs.js

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ describe("syncSpecsLogs", () => {
9696
expect(options.columns[1].alignment).to.equal('center');
9797
expect(options.columns[2].alignment).to.equal('left');
9898
expect(options.columns[1].width).to.equal(1);
99-
expect(options.columns[2].width).to.equal(30);
99+
expect(options.columns[2].width).to.equal(50);
100100
expect(options.columnCount).to.equal(3);
101101
expect(getBorderConfigStub.calledOnce).to.be.true;
102102
});
@@ -259,20 +259,53 @@ describe("syncSpecsLogs", () => {
259259
context("whileProcess", () => {
260260
const whileProcess = syncSpecsLogs.__get__("whileProcess");
261261

262-
it('Should break the loop if request has error', () => {
262+
it('Should retry when request fails with error', () => {
263+
let delayed_n = 2, timeout = 3000, n = 1;
263264
let error = new Error("error");
265+
264266
let requestStub = sandbox.stub();
267+
268+
let postStub = sandbox
269+
.stub(request, "post")
270+
.yields(error, { statusCode: 502 }, JSON.stringify({}));
271+
272+
requestStub.post = postStub;
273+
274+
let setTimeout = sandbox.stub();
275+
syncSpecsLogs.__set__('setTimeout', setTimeout);
276+
syncSpecsLogs.__set__('n', n);
277+
syncSpecsLogs.__set__('timeout', timeout);
278+
syncSpecsLogs.__set__('request', requestStub);
279+
syncSpecsLogs.__set__('whileTries', 5);
280+
281+
let whilstCallback = sandbox.stub();
282+
whileProcess(whilstCallback);
283+
284+
sinon.assert.calledWith(setTimeout, whilstCallback, timeout * delayed_n, null);
285+
expect(syncSpecsLogs.__get__("whileTries")).to.equal(4);
286+
});
287+
288+
it('Should exit after defined number of retries in case of error', () => {
289+
let error = new Error("error"), requestStub = sandbox.stub();
290+
265291
let postStub = sandbox
266292
.stub(request, "post")
267-
.yields(error, { statusCode: 200 }, JSON.stringify({}));
293+
.yields(error, { statusCode: 502 }, JSON.stringify({}));
294+
268295
requestStub.post = postStub;
269296

270297
syncSpecsLogs.__set__('request', requestStub);
298+
syncSpecsLogs.__set__('whileTries', 1);
299+
syncSpecsLogs.__set__('specSummary', {});
300+
syncSpecsLogs.__set__('whileLoop', true);
271301

272302
let whilstCallback = sandbox.stub();
273303
whileProcess(whilstCallback);
274304

275-
sinon.assert.calledWith(whilstCallback, error);
305+
sinon.assert.calledWith(whilstCallback, { status: 504, message: "Tries limit reached" });
306+
expect(syncSpecsLogs.__get__("whileTries")).to.equal(0);
307+
expect(syncSpecsLogs.__get__("whileLoop")).to.equal(false);
308+
expect(syncSpecsLogs.__get__("specSummary.exitCode")).to.equal(2);
276309
});
277310

278311
it('Should print spec details when data is returned from server', () => {

test/unit/bin/helpers/utils.js

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@ const chai = require('chai'),
55
expect = chai.expect,
66
sinon = require('sinon'),
77
chaiAsPromised = require('chai-as-promised'),
8+
chalk = require('chalk'),
89
fs = require('fs');
910

1011
const utils = require('../../../../bin/helpers/utils'),
1112
constant = require('../../../../bin/helpers/constants'),
1213
logger = require('../../../../bin/helpers/logger').winstonLogger,
13-
testObjects = require('../../support/fixtures/testObjects');
14+
testObjects = require('../../support/fixtures/testObjects'),
15+
syncLogger = require("../../../../bin/helpers/logger").syncCliLogger;
1416

1517
chai.use(chaiAsPromised);
1618
logger.transports['console.info'].silent = true;
@@ -943,4 +945,42 @@ describe('utils', () => {
943945
});
944946

945947
});
948+
949+
describe('#handleSyncExit', () => {
950+
let processStub;
951+
beforeEach(function () {
952+
processStub = sinon.stub(process, 'exit');
953+
});
954+
955+
afterEach(function () {
956+
processStub.restore();
957+
});
958+
it('should print network error message when exit code is set to network error code', () => {
959+
let dashboard_url = "dashboard_url", exitCode = 2;
960+
let getNetworkErrorMessageStub = sinon.stub(utils, 'getNetworkErrorMessage');
961+
utils.handleSyncExit(exitCode, dashboard_url);
962+
sinon.assert.calledOnce(getNetworkErrorMessageStub);
963+
sinon.assert.calledOnceWithExactly(processStub, exitCode);
964+
getNetworkErrorMessageStub.restore();
965+
});
966+
967+
it('should print dashboard link when exit code is not network error code', () => {
968+
let dashboard_url = "dashboard_url", exitCode = 1;
969+
let syncCliLoggerStub = sinon.stub(syncLogger, 'info');
970+
utils.handleSyncExit(exitCode, dashboard_url);
971+
sinon.assert.calledTwice(syncCliLoggerStub);
972+
sinon.assert.calledOnceWithExactly(processStub, exitCode);
973+
});
974+
});
975+
976+
describe('#getNetworkErrorMessage', () => {
977+
it('should return the error message in red color', () => {
978+
let dashboard_url = "dashboard_url";
979+
let message = constant.userMessages.FATAL_NETWORK_ERROR + '\n'
980+
+ constant.userMessages.RETRY_LIMIT_EXCEEDED + '\n'
981+
+ constant.userMessages.CHECK_DASHBOARD_AT + dashboard_url
982+
utils.getNetworkErrorMessage(dashboard_url);
983+
expect(utils.getNetworkErrorMessage(dashboard_url)).to.eq(chalk.red(message))
984+
});
985+
});
946986
});

0 commit comments

Comments
 (0)