Skip to content

Commit 9e2cc5a

Browse files
Merge branch 'master' of github.com:browserstack/browserstack-cypress-cli into config_options
2 parents 2e70922 + 0be6b45 commit 9e2cc5a

File tree

11 files changed

+1100
-463
lines changed

11 files changed

+1100
-463
lines changed

bin/commands/generateDownloads.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
'use strict';
2+
3+
const logger = require("../helpers/logger").winstonLogger,
4+
Constants = require("../helpers/constants"),
5+
utils = require("../helpers/utils"),
6+
downloadBuildArtifacts = require('../helpers/buildArtifacts').downloadBuildArtifacts;
7+
8+
9+
module.exports = async function generateDownloads(args) {
10+
let bsConfigPath = utils.getConfigPath(args.cf);
11+
12+
return utils.validateBstackJson(bsConfigPath).then(async function (bsConfig) {
13+
// setting setDefaults to {} if not present and set via env variables or via args.
14+
utils.setDefaults(bsConfig, args);
15+
16+
// accept the username from command line if provided
17+
utils.setUsername(bsConfig, args);
18+
19+
// accept the access key from command line if provided
20+
utils.setAccessKey(bsConfig, args);
21+
22+
utils.setUsageReportingFlag(bsConfig, args.disableUsageReporting);
23+
24+
// set cypress config filename
25+
utils.setCypressConfigFilename(bsConfig, args);
26+
27+
let messageType = Constants.messageTypes.INFO;
28+
let errorCode = null;
29+
let buildId = args._[1];
30+
31+
await downloadBuildArtifacts(bsConfig, buildId, args);
32+
utils.sendUsageReport(bsConfig, args, Constants.usageReportingConstants.GENERATE_DOWNLOADS, messageType, errorCode);
33+
}).catch(function (err) {
34+
logger.error(err);
35+
utils.setUsageReportingFlag(null, args.disableUsageReporting);
36+
utils.sendUsageReport(null, args, err.message, Constants.messageTypes.ERROR, utils.getErrorCodeFromErr(err));
37+
});
38+
};

bin/commands/runs.js

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ const archiver = require("../helpers/archiver"),
1111
syncRunner = require("../helpers/syncRunner"),
1212
checkUploaded = require("../helpers/checkUploaded"),
1313
reportGenerator = require('../helpers/reporterHTML').reportGenerator,
14-
{initTimeComponents, markBlockStart, markBlockEnd, getTimeComponents} = require('../helpers/timeComponents');
14+
{initTimeComponents, markBlockStart, markBlockEnd, getTimeComponents} = require('../helpers/timeComponents'),
15+
downloadBuildArtifacts = require('../helpers/buildArtifacts').downloadBuildArtifacts;
1516

1617
module.exports = function run(args) {
1718
let bsConfigPath = utils.getConfigPath(args.cf);
@@ -73,6 +74,8 @@ module.exports = function run(args) {
7374

7475
//set config (--config)
7576
utils.setConfig(bsConfig, args);
77+
// set other cypress configs e.g. reporter and reporter-options
78+
utils.setOtherConfigs(bsConfig, args);
7679
markBlockEnd('setConfig');
7780

7881
// Validate browserstack.json values and parallels specified via arguments
@@ -142,17 +145,27 @@ module.exports = function run(args) {
142145
// stop the Local instance
143146
await utils.stopLocalBinary(bsConfig, bs_local, args);
144147

148+
// waiting for 5 secs for upload to complete (as a safety measure)
149+
await new Promise(resolve => setTimeout(resolve, 5000));
150+
151+
// download build artifacts
152+
if (utils.nonEmptyArray(bsConfig.run_settings.downloads)) {
153+
await downloadBuildArtifacts(bsConfig, data.build_id, args);
154+
}
155+
145156
// Generate custom report!
146157
reportGenerator(bsConfig, data.build_id, args, function(){
147158
utils.sendUsageReport(bsConfig, args, `${message}\n${dashboardLink}`, Constants.messageTypes.SUCCESS, null);
148159
utils.handleSyncExit(exitCode, data.dashboard_url);
149160
});
150161
});
162+
} else if (utils.nonEmptyArray(bsConfig.run_settings.downloads)) {
163+
logger.info(Constants.userMessages.ASYNC_DOWNLOADS.replace('<build-id>', data.build_id));
151164
}
152165

153166
logger.info(message);
154167
logger.info(dashboardLink);
155-
if(!args.sync) logger.info(Constants.userMessages.EXIT_SYNC_CLI_MESSAGE.replace("<build-id>",data.build_id));
168+
if(!args.sync) logger.info(Constants.userMessages.EXIT_SYNC_CLI_MESSAGE.replace("<build-id>", data.build_id));
156169
let dataToSend = {
157170
time_components: getTimeComponents(),
158171
build_id: data.build_id,

bin/helpers/buildArtifacts.js

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
'use strict';
2+
3+
const fs = require('fs'),
4+
path = require('path');
5+
6+
const axios = require('axios'),
7+
unzipper = require('unzipper');
8+
9+
const logger = require('./logger').winstonLogger,
10+
utils = require("./utils"),
11+
Constants = require("./constants"),
12+
config = require("./config");
13+
14+
15+
let BUILD_ARTIFACTS_TOTAL_COUNT = 0;
16+
let BUILD_ARTIFACTS_FAIL_COUNT = 0;
17+
18+
const parseAndDownloadArtifacts = async (buildId, data) => {
19+
return new Promise(async (resolve, reject) => {
20+
let all_promises = [];
21+
let combs = Object.keys(data);
22+
for(let i = 0; i < combs.length; i++) {
23+
let comb = combs[i];
24+
let sessions = Object.keys(data[comb]);
25+
for(let j = 0; j < sessions.length; j++) {
26+
let sessionId = sessions[j];
27+
let filePath = path.join('./', 'build_artifacts', buildId, comb, sessionId);
28+
let fileName = 'build_artifacts.zip';
29+
BUILD_ARTIFACTS_TOTAL_COUNT += 1;
30+
all_promises.push(downloadAndUnzip(filePath, fileName, data[comb][sessionId]).catch((error) => {
31+
BUILD_ARTIFACTS_FAIL_COUNT += 1;
32+
// delete malformed zip if present
33+
let tmpFilePath = path.join(filePath, fileName);
34+
if(fs.existsSync(tmpFilePath)){
35+
fs.unlinkSync(tmpFilePath);
36+
}
37+
}));
38+
}
39+
}
40+
await Promise.all(all_promises);
41+
resolve();
42+
});
43+
}
44+
45+
const createDirIfNotPresent = async (dir) => {
46+
return new Promise((resolve) => {
47+
if (!fs.existsSync(dir)){
48+
fs.mkdirSync(dir);
49+
}
50+
resolve();
51+
});
52+
}
53+
54+
const createDirectories = async (buildId, data) => {
55+
// create dir for build_artifacts if not already present
56+
let artifactsDir = path.join('./', 'build_artifacts');
57+
if (!fs.existsSync(artifactsDir)){
58+
fs.mkdirSync(artifactsDir);
59+
}
60+
61+
// create dir for buildId if not already present
62+
let buildDir = path.join('./', 'build_artifacts', buildId);
63+
if (fs.existsSync(buildDir)){
64+
// remove dir in case already exists
65+
fs.rmdirSync(buildDir, { recursive: true, force: true });
66+
}
67+
fs.mkdirSync(buildDir);
68+
69+
let combDirs = [];
70+
let sessionDirs = [];
71+
let combs = Object.keys(data);
72+
73+
for(let i = 0; i < combs.length; i++) {
74+
let comb = combs[i];
75+
let combDir = path.join('./', 'build_artifacts', buildId, comb);
76+
combDirs.push(createDirIfNotPresent(combDir));
77+
let sessions = Object.keys(data[comb]);
78+
for(let j = 0; j < sessions.length; j++) {
79+
let sessionId = sessions[j];
80+
let sessionDir = path.join('./', 'build_artifacts', buildId, comb, sessionId);
81+
sessionDirs.push(createDirIfNotPresent(sessionDir));
82+
}
83+
}
84+
85+
return new Promise(async (resolve) => {
86+
// create sub dirs for each combination in build
87+
await Promise.all(combDirs);
88+
// create sub dirs for each machine id in combination
89+
await Promise.all(sessionDirs);
90+
resolve();
91+
});
92+
}
93+
94+
const downloadAndUnzip = async (filePath, fileName, url) => {
95+
let tmpFilePath = path.join(filePath, fileName);
96+
const writer = fs.createWriteStream(tmpFilePath);
97+
98+
return axios({
99+
method: 'get',
100+
url: url,
101+
responseType: 'stream',
102+
}).then(response => {
103+
104+
//ensure that the user can call `then()` only when the file has
105+
//been downloaded entirely.
106+
107+
return new Promise(async (resolve, reject) => {
108+
response.data.pipe(writer);
109+
let error = null;
110+
writer.on('error', err => {
111+
error = err;
112+
writer.close();
113+
reject(err);
114+
});
115+
writer.on('close', async () => {
116+
if (!error) {
117+
await unzipFile(filePath, fileName);
118+
fs.unlinkSync(tmpFilePath);
119+
resolve(true);
120+
}
121+
//no need to call the reject here, as it will have been called in the
122+
//'error' stream;
123+
});
124+
});
125+
});
126+
}
127+
128+
const unzipFile = async (filePath, fileName) => {
129+
return new Promise( async (resolve, reject) => {
130+
await unzipper.Open.file(path.join(filePath, fileName))
131+
.then(d => d.extract({path: filePath, concurrency: 5}))
132+
.catch((err) => reject(err));
133+
resolve();
134+
});
135+
}
136+
137+
const sendUpdatesToBstack = async (bsConfig, buildId, args, options) => {
138+
let url = `${config.buildUrl}${buildId}/build_artifacts/status`;
139+
140+
let cypressJSON = utils.getCypressJSON(bsConfig);
141+
142+
let reporter = null;
143+
if(!utils.isUndefined(args.reporter)) {
144+
reporter = args.reporter;
145+
} else if(cypressJSON !== undefined){
146+
reporter = cypressJSON.reporter;
147+
}
148+
149+
let data = {
150+
feature_usage: {
151+
downloads: {
152+
eligible_download_folders: BUILD_ARTIFACTS_TOTAL_COUNT,
153+
successfully_downloaded_folders: BUILD_ARTIFACTS_TOTAL_COUNT - BUILD_ARTIFACTS_FAIL_COUNT
154+
},
155+
reporter: reporter
156+
}
157+
}
158+
159+
try {
160+
await axios.post(url, data, options);
161+
} catch (err) {
162+
utils.sendUsageReport(bsConfig, args, err, Constants.messageTypes.ERROR, 'api_failed_build_artifacts_status_update');
163+
}
164+
}
165+
166+
exports.downloadBuildArtifacts = async (bsConfig, buildId, args) => {
167+
BUILD_ARTIFACTS_FAIL_COUNT = 0;
168+
BUILD_ARTIFACTS_TOTAL_COUNT = 0;
169+
170+
let url = `${config.buildUrl}${buildId}/build_artifacts`;
171+
let options = {
172+
auth: {
173+
username: bsConfig.auth.username,
174+
password: bsConfig.auth.access_key,
175+
},
176+
headers: {
177+
'User-Agent': utils.getUserAgent(),
178+
},
179+
};
180+
181+
let message = null;
182+
let messageType = null;
183+
let errorCode = null;
184+
185+
try {
186+
const res = await axios.get(url, options);
187+
let buildDetails = res.data;
188+
189+
await createDirectories(buildId, buildDetails);
190+
await parseAndDownloadArtifacts(buildId, buildDetails);
191+
192+
if (BUILD_ARTIFACTS_FAIL_COUNT > 0) {
193+
messageType = Constants.messageTypes.ERROR;
194+
message = Constants.userMessages.DOWNLOAD_BUILD_ARTIFACTS_FAILED.replace('<build-id>', buildId).replace('<machine-count>', BUILD_ARTIFACTS_FAIL_COUNT);
195+
logger.error(message);
196+
} else {
197+
messageType = Constants.messageTypes.SUCCESS;
198+
message = Constants.userMessages.DOWNLOAD_BUILD_ARTIFACTS_SUCCESS.replace('<build-id>', buildId).replace('<user-path>', process.cwd());
199+
logger.info(message);
200+
}
201+
202+
await sendUpdatesToBstack(bsConfig, buildId, args, options);
203+
utils.sendUsageReport(bsConfig, args, message, messageType, null);
204+
} catch (err) {
205+
messageType = Constants.messageTypes.ERROR;
206+
errorCode = 'api_failed_build_artifacts';
207+
208+
if (BUILD_ARTIFACTS_FAIL_COUNT > 0) {
209+
messageType = Constants.messageTypes.ERROR;
210+
message = Constants.userMessages.DOWNLOAD_BUILD_ARTIFACTS_FAILED.replace('<build-id>', buildId).replace('<machine-count>', BUILD_ARTIFACTS_FAIL_COUNT);
211+
logger.error(message);
212+
} else {
213+
logger.error('Downloading the build artifacts failed.');
214+
}
215+
216+
utils.sendUsageReport(bsConfig, args, err, messageType, errorCode);
217+
}
218+
};

bin/helpers/constants.js

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ const userMessages = {
4545
LOCAL_STOP_FAILED: "Local Binary stop failed.",
4646
INVALID_LOCAL_MODE_WARNING: "Invalid value specified for local_mode. local_mode: (\"always-on\" | \"on-demand\"). For more info, check out https://www.browserstack.com/docs/automate/cypress/cli-reference",
4747
SPEC_LIMIT_WARNING: "You might not see all your results on the dashboard because of high spec count, please consider reducing the number of spec files in this folder.",
48+
DOWNLOAD_BUILD_ARTIFACTS_FAILED: "Downloading build artifacts for the build <build-id> failed for <machine-count> machines.",
49+
ASYNC_DOWNLOADS: "Test artifacts as specified under 'downloads' can be downloaded after the build has completed its run, using 'browserstack-cypress generate-downloads <build-id>'",
50+
DOWNLOAD_BUILD_ARTIFACTS_SUCCESS: "Your build artifact(s) have been successfully downloaded in '<user-path>/build_artifacts/<build-id>' directory",
4851
LATEST_SYNTAX_TO_ACTUAL_VERSION_MESSAGE: "Your build will run using Cypress <actualVersion> as you had specified <latestSyntaxVersion>. Read more about supported versions here: http://browserstack.com/docs/automate/cypress/supported-versions"
4952
};
5053

@@ -87,17 +90,13 @@ const cliMessages = {
8790
INFO: "Check status of your build.",
8891
STOP: "Stop your build.",
8992
DEMAND: "Requires a build id.",
90-
DESC: "Path to BrowserStack config",
91-
CONFIG_DEMAND: "config file is required",
9293
INFO_MESSAGE: "Getting information for buildId ",
9394
STOP_MESSAGE: "Stopping build with given buildId ",
9495
},
9596
RUN: {
9697
PARALLEL_DESC: "The maximum number of parallels to use to run your test suite",
9798
INFO: "Run your tests on BrowserStack.",
98-
DESC: "Path to BrowserStack config",
9999
CYPRESS_DESC: "Path to Cypress config file",
100-
CONFIG_DEMAND: "config file is required",
101100
CYPRESS_CONFIG_DEMAND: "Cypress config file is required",
102101
BUILD_NAME: "The build name you want to use to name your test runs",
103102
EXCLUDE: "Exclude files matching a pattern from zipping and uploading",
@@ -113,18 +112,25 @@ const cliMessages = {
113112
LOCAL_CONFIG_FILE: "Accepted values: String - path to local config-file to your Local process instance. Learn more at https://www.browserstack.com/local-testing/binary-params",
114113
SYNC_NO_WRAP: "Wrap the spec names in --sync mode in case of smaller terminal window size pass --no-wrap",
115114
BROWSER_DESCRIPTION: "Specify the browsers you need to run your tests on.",
116-
CONFIG_DESCRIPTION: "Set configuration values. Separate multiple values with a comma. The values set here override any values set in your configuration file."
115+
CONFIG_DESCRIPTION: "Set configuration values. Separate multiple values with a comma. The values set here override any values set in your configuration file.",
116+
REPORTER: "Specify the custom reporter to use",
117+
REPORTER_OPTIONS: "Specify reporter options for custom reporter",
117118
},
118119
COMMON: {
119120
DISABLE_USAGE_REPORTING: "Disable usage reporting",
120121
FORCE_UPLOAD: "Force the upload of your test files even if BrowserStack has detected no changes in your suite since you last ran",
121122
USERNAME: "Your BrowserStack username",
122123
ACCESS_KEY: "Your BrowserStack access key",
123124
NO_NPM_WARNING: "No NPM warning if npm_dependencies is empty",
125+
CONFIG_DEMAND: "config file is required",
126+
CONFIG_FILE_PATH: "Path to BrowserStack config",
124127
},
125128
GENERATE_REPORT: {
126129
INFO: "Generates the build report"
127130
},
131+
GENERATE_DOWNLOADS: {
132+
INFO: "Downloads the build artifacts"
133+
},
128134
};
129135

130136
const messageTypes = {
@@ -150,6 +156,7 @@ const filesToIgnoreWhileUploading = [
150156
'.vscode/**',
151157
'.npm/**',
152158
'.yarn/**',
159+
'build_artifacts/**'
153160
];
154161

155162
const readDirOptions = {
@@ -174,6 +181,10 @@ const DEFAULT_CYPRESS_SPEC_PATH = "cypress/integration"
174181
const SPEC_TOTAL_CHAR_LIMIT = 32243;
175182
const METADATA_CHAR_BUFFER_PER_SPEC = 175;
176183

184+
const usageReportingConstants = {
185+
GENERATE_DOWNLOADS: 'generate-downloads called',
186+
}
187+
177188
const LATEST_VERSION_SYNTAX_REGEX = /\d*.latest(.\d*)?/gm
178189

179190
module.exports = Object.freeze({
@@ -190,5 +201,6 @@ module.exports = Object.freeze({
190201
DEFAULT_CYPRESS_SPEC_PATH,
191202
SPEC_TOTAL_CHAR_LIMIT,
192203
METADATA_CHAR_BUFFER_PER_SPEC,
204+
usageReportingConstants,
193205
LATEST_VERSION_SYNTAX_REGEX
194206
});

0 commit comments

Comments
 (0)