diff --git a/bin/commands/generateDownloads.js b/bin/commands/generateDownloads.js new file mode 100644 index 00000000..b81d3feb --- /dev/null +++ b/bin/commands/generateDownloads.js @@ -0,0 +1,38 @@ +'use strict'; + +const logger = require("../helpers/logger").winstonLogger, + Constants = require("../helpers/constants"), + utils = require("../helpers/utils"), + downloadBuildArtifacts = require('../helpers/buildArtifacts').downloadBuildArtifacts; + + +module.exports = async function generateDownloads(args) { + let bsConfigPath = utils.getConfigPath(args.cf); + + return utils.validateBstackJson(bsConfigPath).then(async function (bsConfig) { + // setting setDefaults to {} if not present and set via env variables or via args. + utils.setDefaults(bsConfig, args); + + // accept the username from command line if provided + utils.setUsername(bsConfig, args); + + // accept the access key from command line if provided + utils.setAccessKey(bsConfig, args); + + utils.setUsageReportingFlag(bsConfig, args.disableUsageReporting); + + // set cypress config filename + utils.setCypressConfigFilename(bsConfig, args); + + let messageType = Constants.messageTypes.INFO; + let errorCode = null; + let buildId = args._[1]; + + await downloadBuildArtifacts(bsConfig, buildId, args); + utils.sendUsageReport(bsConfig, args, Constants.usageReportingConstants.GENERATE_DOWNLOADS, messageType, errorCode); + }).catch(function (err) { + logger.error(err); + utils.setUsageReportingFlag(null, args.disableUsageReporting); + utils.sendUsageReport(null, args, err.message, Constants.messageTypes.ERROR, utils.getErrorCodeFromErr(err)); + }); +}; diff --git a/bin/commands/runs.js b/bin/commands/runs.js index a0513e92..bd14f3e2 100644 --- a/bin/commands/runs.js +++ b/bin/commands/runs.js @@ -11,7 +11,8 @@ const archiver = require("../helpers/archiver"), syncRunner = require("../helpers/syncRunner"), checkUploaded = require("../helpers/checkUploaded"), reportGenerator = require('../helpers/reporterHTML').reportGenerator, - {initTimeComponents, markBlockStart, markBlockEnd, getTimeComponents} = require('../helpers/timeComponents'); + {initTimeComponents, markBlockStart, markBlockEnd, getTimeComponents} = require('../helpers/timeComponents'), + downloadBuildArtifacts = require('../helpers/buildArtifacts').downloadBuildArtifacts; module.exports = function run(args) { let bsConfigPath = utils.getConfigPath(args.cf); @@ -67,6 +68,9 @@ module.exports = function run(args) { // set the no-wrap utils.setNoWrap(bsConfig, args); + + // set other cypress configs e.g. reporter and reporter-options + utils.setOtherConfigs(bsConfig, args); markBlockEnd('setConfig'); // Validate browserstack.json values and parallels specified via arguments @@ -136,17 +140,27 @@ module.exports = function run(args) { // stop the Local instance await utils.stopLocalBinary(bsConfig, bs_local, args); + // waiting for 5 secs for upload to complete (as a safety measure) + await new Promise(resolve => setTimeout(resolve, 5000)); + + // download build artifacts + if (utils.nonEmptyArray(bsConfig.run_settings.downloads)) { + await downloadBuildArtifacts(bsConfig, data.build_id, args); + } + // Generate custom report! reportGenerator(bsConfig, data.build_id, args, function(){ utils.sendUsageReport(bsConfig, args, `${message}\n${dashboardLink}`, Constants.messageTypes.SUCCESS, null); utils.handleSyncExit(exitCode, data.dashboard_url); }); }); + } else if (utils.nonEmptyArray(bsConfig.run_settings.downloads)) { + logger.info(Constants.userMessages.ASYNC_DOWNLOADS.replace('', data.build_id)); } logger.info(message); logger.info(dashboardLink); - if(!args.sync) logger.info(Constants.userMessages.EXIT_SYNC_CLI_MESSAGE.replace("",data.build_id)); + if(!args.sync) logger.info(Constants.userMessages.EXIT_SYNC_CLI_MESSAGE.replace("", data.build_id)); let dataToSend = { time_components: getTimeComponents(), build_id: data.build_id, diff --git a/bin/helpers/buildArtifacts.js b/bin/helpers/buildArtifacts.js new file mode 100644 index 00000000..886e0f17 --- /dev/null +++ b/bin/helpers/buildArtifacts.js @@ -0,0 +1,218 @@ +'use strict'; + +const fs = require('fs'), + path = require('path'); + +const axios = require('axios'), + unzipper = require('unzipper'); + +const logger = require('./logger').winstonLogger, + utils = require("./utils"), + Constants = require("./constants"), + config = require("./config"); + + +let BUILD_ARTIFACTS_TOTAL_COUNT = 0; +let BUILD_ARTIFACTS_FAIL_COUNT = 0; + +const parseAndDownloadArtifacts = async (buildId, data) => { + return new Promise(async (resolve, reject) => { + let all_promises = []; + let combs = Object.keys(data); + for(let i = 0; i < combs.length; i++) { + let comb = combs[i]; + let sessions = Object.keys(data[comb]); + for(let j = 0; j < sessions.length; j++) { + let sessionId = sessions[j]; + let filePath = path.join('./', 'build_artifacts', buildId, comb, sessionId); + let fileName = 'build_artifacts.zip'; + BUILD_ARTIFACTS_TOTAL_COUNT += 1; + all_promises.push(downloadAndUnzip(filePath, fileName, data[comb][sessionId]).catch((error) => { + BUILD_ARTIFACTS_FAIL_COUNT += 1; + // delete malformed zip if present + let tmpFilePath = path.join(filePath, fileName); + if(fs.existsSync(tmpFilePath)){ + fs.unlinkSync(tmpFilePath); + } + })); + } + } + await Promise.all(all_promises); + resolve(); + }); +} + +const createDirIfNotPresent = async (dir) => { + return new Promise((resolve) => { + if (!fs.existsSync(dir)){ + fs.mkdirSync(dir); + } + resolve(); + }); +} + +const createDirectories = async (buildId, data) => { + // create dir for build_artifacts if not already present + let artifactsDir = path.join('./', 'build_artifacts'); + if (!fs.existsSync(artifactsDir)){ + fs.mkdirSync(artifactsDir); + } + + // create dir for buildId if not already present + let buildDir = path.join('./', 'build_artifacts', buildId); + if (fs.existsSync(buildDir)){ + // remove dir in case already exists + fs.rmdirSync(buildDir, { recursive: true, force: true }); + } + fs.mkdirSync(buildDir); + + let combDirs = []; + let sessionDirs = []; + let combs = Object.keys(data); + + for(let i = 0; i < combs.length; i++) { + let comb = combs[i]; + let combDir = path.join('./', 'build_artifacts', buildId, comb); + combDirs.push(createDirIfNotPresent(combDir)); + let sessions = Object.keys(data[comb]); + for(let j = 0; j < sessions.length; j++) { + let sessionId = sessions[j]; + let sessionDir = path.join('./', 'build_artifacts', buildId, comb, sessionId); + sessionDirs.push(createDirIfNotPresent(sessionDir)); + } + } + + return new Promise(async (resolve) => { + // create sub dirs for each combination in build + await Promise.all(combDirs); + // create sub dirs for each machine id in combination + await Promise.all(sessionDirs); + resolve(); + }); +} + +const downloadAndUnzip = async (filePath, fileName, url) => { + let tmpFilePath = path.join(filePath, fileName); + const writer = fs.createWriteStream(tmpFilePath); + + return axios({ + method: 'get', + url: url, + responseType: 'stream', + }).then(response => { + + //ensure that the user can call `then()` only when the file has + //been downloaded entirely. + + return new Promise(async (resolve, reject) => { + response.data.pipe(writer); + let error = null; + writer.on('error', err => { + error = err; + writer.close(); + reject(err); + }); + writer.on('close', async () => { + if (!error) { + await unzipFile(filePath, fileName); + fs.unlinkSync(tmpFilePath); + resolve(true); + } + //no need to call the reject here, as it will have been called in the + //'error' stream; + }); + }); + }); +} + +const unzipFile = async (filePath, fileName) => { + return new Promise( async (resolve, reject) => { + await unzipper.Open.file(path.join(filePath, fileName)) + .then(d => d.extract({path: filePath, concurrency: 5})) + .catch((err) => reject(err)); + resolve(); + }); +} + +const sendUpdatesToBstack = async (bsConfig, buildId, args, options) => { + let url = `${config.buildUrl}${buildId}/build_artifacts/status`; + + let cypressJSON = utils.getCypressJSON(bsConfig); + + let reporter = null; + if(!utils.isUndefined(args.reporter)) { + reporter = args.reporter; + } else if(cypressJSON !== undefined){ + reporter = cypressJSON.reporter; + } + + let data = { + feature_usage: { + downloads: { + eligible_download_folders: BUILD_ARTIFACTS_TOTAL_COUNT, + successfully_downloaded_folders: BUILD_ARTIFACTS_TOTAL_COUNT - BUILD_ARTIFACTS_FAIL_COUNT + }, + reporter: reporter + } + } + + try { + await axios.post(url, data, options); + } catch (err) { + utils.sendUsageReport(bsConfig, args, err, Constants.messageTypes.ERROR, 'api_failed_build_artifacts_status_update'); + } +} + +exports.downloadBuildArtifacts = async (bsConfig, buildId, args) => { + BUILD_ARTIFACTS_FAIL_COUNT = 0; + BUILD_ARTIFACTS_TOTAL_COUNT = 0; + + let url = `${config.buildUrl}${buildId}/build_artifacts`; + let options = { + auth: { + username: bsConfig.auth.username, + password: bsConfig.auth.access_key, + }, + headers: { + 'User-Agent': utils.getUserAgent(), + }, + }; + + let message = null; + let messageType = null; + let errorCode = null; + + try { + const res = await axios.get(url, options); + let buildDetails = res.data; + + await createDirectories(buildId, buildDetails); + await parseAndDownloadArtifacts(buildId, buildDetails); + + if (BUILD_ARTIFACTS_FAIL_COUNT > 0) { + messageType = Constants.messageTypes.ERROR; + message = Constants.userMessages.DOWNLOAD_BUILD_ARTIFACTS_FAILED.replace('', buildId).replace('', BUILD_ARTIFACTS_FAIL_COUNT); + logger.error(message); + } else { + messageType = Constants.messageTypes.SUCCESS; + message = Constants.userMessages.DOWNLOAD_BUILD_ARTIFACTS_SUCCESS.replace('', buildId).replace('', process.cwd()); + logger.info(message); + } + + await sendUpdatesToBstack(bsConfig, buildId, args, options); + utils.sendUsageReport(bsConfig, args, message, messageType, null); + } catch (err) { + messageType = Constants.messageTypes.ERROR; + errorCode = 'api_failed_build_artifacts'; + + if (BUILD_ARTIFACTS_FAIL_COUNT > 0) { + messageType = Constants.messageTypes.ERROR; + message = Constants.userMessages.DOWNLOAD_BUILD_ARTIFACTS_FAILED.replace('', buildId).replace('', BUILD_ARTIFACTS_FAIL_COUNT); + logger.error(message); + } else { + logger.error('Downloading the build artifacts failed.'); + } + + utils.sendUsageReport(bsConfig, args, err, messageType, errorCode); + } +}; diff --git a/bin/helpers/constants.js b/bin/helpers/constants.js index 884687c4..6b56f304 100644 --- a/bin/helpers/constants.js +++ b/bin/helpers/constants.js @@ -45,6 +45,9 @@ const userMessages = { LOCAL_STOP_FAILED: "Local Binary stop failed.", 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", 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.", + DOWNLOAD_BUILD_ARTIFACTS_FAILED: "Downloading build artifacts for the build failed for machines.", + ASYNC_DOWNLOADS: "Test artifacts as specified under 'downloads' can be downloaded after the build has completed its run, using 'browserstack-cypress generate-downloads '", + DOWNLOAD_BUILD_ARTIFACTS_SUCCESS: "Your build artifact(s) have been successfully downloaded in '/build_artifacts/' directory", LATEST_SYNTAX_TO_ACTUAL_VERSION_MESSAGE: "Your build will run using Cypress as you had specified . Read more about supported versions here: http://browserstack.com/docs/automate/cypress/supported-versions" }; @@ -86,17 +89,13 @@ const cliMessages = { INFO: "Check status of your build.", STOP: "Stop your build.", DEMAND: "Requires a build id.", - DESC: "Path to BrowserStack config", - CONFIG_DEMAND: "config file is required", INFO_MESSAGE: "Getting information for buildId ", STOP_MESSAGE: "Stopping build with given buildId ", }, RUN: { PARALLEL_DESC: "The maximum number of parallels to use to run your test suite", INFO: "Run your tests on BrowserStack.", - DESC: "Path to BrowserStack config", CYPRESS_DESC: "Path to Cypress config file", - CONFIG_DEMAND: "config file is required", CYPRESS_CONFIG_DEMAND: "Cypress config file is required", BUILD_NAME: "The build name you want to use to name your test runs", EXCLUDE: "Exclude files matching a pattern from zipping and uploading", @@ -110,7 +109,9 @@ const cliMessages = { LOCAL_MODE: 'Accepted values: ("always-on" | "on-demand") - if you choose to keep the binary "always-on", it will speed up your tests by keeping the Local connection warmed up in the background; otherwise, you can choose to have it spawn and killed for every build', LOCAL_IDENTIFIER: "Accepted values: String - assign an identifier to your Local process instance", 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", - SYNC_NO_WRAP: "Wrap the spec names in --sync mode in case of smaller terminal window size pass --no-wrap" + SYNC_NO_WRAP: "Wrap the spec names in --sync mode in case of smaller terminal window size pass --no-wrap", + REPORTER: "Specify the custom reporter to use", + REPORTER_OPTIONS: "Specify reporter options for custom reporter", }, COMMON: { DISABLE_USAGE_REPORTING: "Disable usage reporting", @@ -118,10 +119,15 @@ const cliMessages = { USERNAME: "Your BrowserStack username", ACCESS_KEY: "Your BrowserStack access key", NO_NPM_WARNING: "No NPM warning if npm_dependencies is empty", + CONFIG_DEMAND: "config file is required", + CONFIG_FILE_PATH: "Path to BrowserStack config", }, GENERATE_REPORT: { INFO: "Generates the build report" }, + GENERATE_DOWNLOADS: { + INFO: "Downloads the build artifacts" + }, }; const messageTypes = { @@ -147,6 +153,7 @@ const filesToIgnoreWhileUploading = [ '.vscode/**', '.npm/**', '.yarn/**', + 'build_artifacts/**' ]; const readDirOptions = { @@ -171,6 +178,10 @@ const DEFAULT_CYPRESS_SPEC_PATH = "cypress/integration" const SPEC_TOTAL_CHAR_LIMIT = 32243; const METADATA_CHAR_BUFFER_PER_SPEC = 175; +const usageReportingConstants = { + GENERATE_DOWNLOADS: 'generate-downloads called', +} + const LATEST_VERSION_SYNTAX_REGEX = /\d*.latest(.\d*)?/gm module.exports = Object.freeze({ @@ -187,5 +198,6 @@ module.exports = Object.freeze({ DEFAULT_CYPRESS_SPEC_PATH, SPEC_TOTAL_CHAR_LIMIT, METADATA_CHAR_BUFFER_PER_SPEC, + usageReportingConstants, LATEST_VERSION_SYNTAX_REGEX }); diff --git a/bin/helpers/utils.js b/bin/helpers/utils.js index 345e18ff..44eacbc1 100644 --- a/bin/helpers/utils.js +++ b/bin/helpers/utils.js @@ -140,6 +140,7 @@ exports.setParallels = (bsConfig, args, numOfSpecs) => { let maxParallels = browserCombinations.length * numOfSpecs; if (numOfSpecs <= 0) { bsConfig['run_settings']['parallels'] = browserCombinations.length; + bsConfig['run_settings']['specs_count'] = numOfSpecs; return; } if (bsConfig['run_settings']['parallels'] > maxParallels && bsConfig['run_settings']['parallels'] != -1 ) { @@ -147,6 +148,7 @@ exports.setParallels = (bsConfig, args, numOfSpecs) => { `Using ${maxParallels} machines instead of ${bsConfig['run_settings']['parallels']} that you configured as there are ${numOfSpecs} specs to be run on ${browserCombinations.length} browser combinations.` ); bsConfig['run_settings']['parallels'] = maxParallels; + bsConfig['run_settings']['specs_count'] = numOfSpecs; } }; @@ -333,6 +335,13 @@ exports.isUndefined = value => (value === undefined || value === null); exports.isFloat = (value) => Number(value) && Number(value) % 1 !== 0; +exports.nonEmptyArray = (value) => { + if(!this.isUndefined(value) && value && value.length) { + return true; + } + return false; +} + exports.isParallelValid = (value) => { return this.isUndefined(value) || !(isNaN(value) || this.isFloat(value) || parseInt(value, 10) === 0 || parseInt(value, 10) < -1) || value === Constants.cliMessages.RUN.DEFAULT_PARALLEL_MESSAGE; } @@ -745,3 +754,26 @@ exports.deleteBaseUrlFromError = (err) => { return err.replace(/To test ([\s\S]*)on BrowserStack/g, 'To test on BrowserStack'); } +// blindly send other passed configs with run_settings and handle at backend +exports.setOtherConfigs = (bsConfig, args) => { + if (!this.isUndefined(args.reporter)) { + bsConfig["run_settings"]["reporter"] = args.reporter; + } + if (!this.isUndefined(args.reporterOptions)) { + bsConfig["run_settings"]["reporter_options"] = args.reporterOptions; + } +} + +exports.getCypressJSON = (bsConfig) => { + let cypressJSON = undefined; + if (bsConfig.run_settings.cypress_config_file && bsConfig.run_settings.cypress_config_filename !== 'false') { + cypressJSON = JSON.parse( + fs.readFileSync(bsConfig.run_settings.cypressConfigFilePath) + ); + } else if (bsConfig.run_settings.cypressProjectDir) { + cypressJSON = JSON.parse( + fs.readFileSync(path.join(bsConfig.run_settings.cypressProjectDir, 'cypress.json')) + ); + } + return cypressJSON; +} diff --git a/bin/runner.js b/bin/runner.js index 0e35b1f0..c1267ddf 100755 --- a/bin/runner.js +++ b/bin/runner.js @@ -4,6 +4,52 @@ const yargs = require('yargs'), logger = require("./helpers/logger").winstonLogger, Constants = require('./helpers/constants'); + +const disableUsageReportingOptions = { + 'disable-usage-reporting': { + default: undefined, + description: Constants.cliMessages.COMMON.DISABLE_USAGE_REPORTING, + type: "boolean" + }, +} + +const usernameOptions = { + 'u': { + alias: 'username', + describe: Constants.cliMessages.COMMON.USERNAME, + type: "string", + default: undefined + }, +} + +const accessKeyOptions = { + 'k': { + alias: 'key', + describe: Constants.cliMessages.COMMON.ACCESS_KEY, + type: "string", + default: undefined + }, +} + +const configFileOptions = { + 'cf': { + alias: 'config-file', + describe: Constants.cliMessages.COMMON.CONFIG_FILE_PATH, + default: 'browserstack.json', + type: 'string', + nargs: 1, + demand: true, + demand: Constants.cliMessages.COMMON.CONFIG_DEMAND + }, +} + +const commonBuildOptions = { + ...configFileOptions, + ...disableUsageReportingOptions, + ...usernameOptions, + ...accessKeyOptions, +} + function checkCommands(yargs, argv, numRequired) { if (argv._.length < numRequired) { yargs.showHelp() @@ -24,17 +70,13 @@ var argv = yargs argv = yargs .usage("usage: $0 init [filename] [options]") .options({ + ...disableUsageReportingOptions, 'p': { alias: "path", default: false, description: Constants.cliMessages.INIT.DESC, type: "string", }, - 'disable-usage-reporting': { - default: undefined, - description: Constants.cliMessages.COMMON.DISABLE_USAGE_REPORTING, - type: "boolean" - }, }) .help("help") .wrap(null).argv; @@ -48,32 +90,7 @@ var argv = yargs .usage('usage: $0 ') .demand(1, Constants.cliMessages.BUILD.DEMAND) .options({ - 'cf': { - alias: 'config-file', - describe: Constants.cliMessages.BUILD.DESC, - default: 'browserstack.json', - type: 'string', - nargs: 1, - demand: true, - demand: Constants.cliMessages.BUILD.CONFIG_DEMAND - }, - 'disable-usage-reporting': { - default: undefined, - description: Constants.cliMessages.COMMON.DISABLE_USAGE_REPORTING, - type: "boolean" - }, - 'u': { - alias: 'username', - describe: Constants.cliMessages.COMMON.USERNAME, - type: "string", - default: undefined - }, - 'k': { - alias: 'key', - describe: Constants.cliMessages.COMMON.ACCESS_KEY, - type: "string", - default: undefined - }, + ...commonBuildOptions, }) .help('help') .wrap(null) @@ -88,32 +105,7 @@ var argv = yargs .usage('usage: $0 ') .demand(1, Constants.cliMessages.BUILD.DEMAND) .options({ - 'cf': { - alias: 'config-file', - describe: Constants.cliMessages.BUILD.DESC, - default: 'browserstack.json', - type: 'string', - nargs: 1, - demand: true, - demand: Constants.cliMessages.BUILD.CONFIG_DEMAND - }, - 'disable-usage-reporting': { - default: undefined, - description: Constants.cliMessages.COMMON.DISABLE_USAGE_REPORTING, - type: "boolean" - }, - 'u': { - alias: 'username', - describe: Constants.cliMessages.COMMON.USERNAME, - type: "string", - default: undefined - }, - 'k': { - alias: 'key', - describe: Constants.cliMessages.COMMON.ACCESS_KEY, - type: "string", - default: undefined - }, + ...commonBuildOptions, }) .help('help') .wrap(null) @@ -127,15 +119,7 @@ var argv = yargs argv = yargs .usage('usage: $0 run ') .options({ - 'cf': { - alias: 'config-file', - describe: Constants.cliMessages.RUN.DESC, - default: 'browserstack.json', - type: 'string', - nargs: 1, - demand: true, - demand: Constants.cliMessages.RUN.CONFIG_DEMAND - }, + ...commonBuildOptions, 'ccf': { alias: 'cypress-config-file', describe: Constants.cliMessages.RUN.CYPRESS_DESC, @@ -145,29 +129,12 @@ var argv = yargs demand: true, demand: Constants.cliMessages.RUN.CYPRESS_CONFIG_DEMAND }, - 'disable-usage-reporting': { - default: undefined, - description: Constants.cliMessages.COMMON.DISABLE_USAGE_REPORTING, - type: "boolean" - }, 'p': { alias: 'parallels', describe: Constants.cliMessages.RUN.PARALLEL_DESC, type: "number", default: undefined }, - 'u': { - alias: 'username', - describe: Constants.cliMessages.COMMON.USERNAME, - type: "string", - default: undefined - }, - 'k': { - alias: 'key', - describe: Constants.cliMessages.COMMON.ACCESS_KEY, - type: "string", - default: undefined - }, 'b': { alias: 'build-name', describe: Constants.cliMessages.RUN.BUILD_NAME, @@ -231,7 +198,19 @@ var argv = yargs default: false, describe: Constants.cliMessages.RUN.SYNC_NO_WRAP, type: "boolean" - } + }, + 'r': { + alias: 'reporter', + default: undefined, + describe: Constants.cliMessages.RUN.REPORTER, + type: "string" + }, + 'o': { + alias: 'reporter-options', + default: undefined, + describe: Constants.cliMessages.RUN.REPORTER_OPTIONS, + type: "string" + }, }) .help('help') .wrap(null) @@ -245,32 +224,7 @@ var argv = yargs .usage('usage: $0 generate-report ') .demand(1, Constants.cliMessages.BUILD.DEMAND) .options({ - 'cf': { - alias: 'config-file', - describe: Constants.cliMessages.BUILD.DESC, - default: 'browserstack.json', - type: 'string', - nargs: 1, - demand: true, - demand: Constants.cliMessages.BUILD.CONFIG_DEMAND - }, - 'disable-usage-reporting': { - default: undefined, - description: Constants.cliMessages.COMMON.DISABLE_USAGE_REPORTING, - type: "boolean" - }, - 'u': { - alias: 'username', - describe: Constants.cliMessages.COMMON.USERNAME, - type: "string", - default: undefined - }, - 'k': { - alias: 'key', - describe: Constants.cliMessages.COMMON.ACCESS_KEY, - type: "string", - default: undefined - }, + ...commonBuildOptions, }) .help('help') .wrap(null) @@ -280,6 +234,21 @@ var argv = yargs return require('./commands/generateReport')(argv); } }) + .command('generate-downloads', Constants.cliMessages.GENERATE_DOWNLOADS.INFO, function(yargs) { + argv = yargs + .usage('usage: $0 generate-downloads ') + .demand(1, Constants.cliMessages.BUILD.DEMAND) + .options({ + ...commonBuildOptions, + }) + .help('help') + .wrap(null) + .argv + if (checkCommands(yargs, argv, 1)) { + logger.info(Constants.cliMessages.BUILD.INFO_MESSAGE + argv._[1]); + return require('./commands/generateDownloads')(argv); + } + }) .help('help') .wrap(null) .argv diff --git a/package.json b/package.json index ba19f5b4..9fcf669c 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "url": "https://github.com/browserstack/browserstack-cypress-cli/issues" }, "devDependencies": { + "axios": "^0.21.1", "chai": "^4.2.0", "chai-as-promised": "^7.1.1", "custom-env": "^2.0.1", @@ -44,6 +45,7 @@ "nyc": "^15.0.1", "proxyquire": "^2.1.3", "rewire": "^5.0.0", - "sinon": "^9.0.2" + "sinon": "^9.0.2", + "unzipper": "^0.10.11" } } diff --git a/test/unit/bin/commands/generateDownloads.js b/test/unit/bin/commands/generateDownloads.js new file mode 100644 index 00000000..863dcdeb --- /dev/null +++ b/test/unit/bin/commands/generateDownloads.js @@ -0,0 +1,114 @@ +const chai = require("chai"), + chaiAsPromised = require("chai-as-promised"), + sinon = require('sinon'); +const { downloadBuildArtifacts } = require("../../../../bin/helpers/buildArtifacts"); +const constants = require("../../../../bin/helpers/constants"); + +const Constants = require("../../../../bin/helpers/constants"), + logger = require("../../../../bin/helpers/logger").winstonLogger, + testObjects = require("../../support/fixtures/testObjects"); + +const proxyquire = require("proxyquire").noCallThru(); + +chai.use(chaiAsPromised); +logger.transports["console.info"].silent = true; + +describe("generateDownloads", () => { + let args = testObjects.generateDownloadsInputArgs; + let body = testObjects.buildInfoSampleBody; + let bsConfig = testObjects.sampleBsConfig; + + describe("Calling downloadBuildArtifacts", () => { + var sandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + + getConfigPathStub = sandbox.stub(); + validateBstackJsonStub = sandbox.stub(); + setDefaultAuthHashStub = sandbox.stub(); + setUsernameStub = sandbox.stub(); + setAccessKeyStub = sandbox.stub(); + setUsageReportingFlagStub = sandbox.stub().returns(undefined); + setCypressConfigFilenameStub = sandbox.stub(); + sendUsageReportStub = sandbox.stub().callsFake(function () { + return "end"; + }); + + downloadBuildArtifactsSpy = sandbox.spy(); + getErrorCodeFromErrStub = sandbox.stub().returns("random-error"); + setDefaultsStub = sandbox.stub(); + }); + + afterEach(() => { + sandbox.restore(); + sinon.restore(); + }); + + it("calls downloadBuildArtifacts", () => { + const generateReport = proxyquire('../../../../bin/commands/generateDownloads', { + '../helpers/utils': { + getConfigPath: getConfigPathStub, + validateBstackJson: validateBstackJsonStub, + setDefaultAuthHash: setDefaultAuthHashStub, + setUsername: setUsernameStub, + setAccessKey: setAccessKeyStub, + setUsageReportingFlag: setUsageReportingFlagStub, + setCypressConfigFilename: setCypressConfigFilenameStub, + sendUsageReport: sendUsageReportStub, + setDefaults: setDefaultsStub, + getErrorCodeFromErr: getErrorCodeFromErrStub + }, + '../helpers/buildArtifacts': { + downloadBuildArtifacts: downloadBuildArtifactsSpy + } + }); + + validateBstackJsonStub.returns(Promise.resolve(bsConfig)); + + generateReport(args) + .then(function (_bsConfig) { + sinon.assert.calledWith(downloadBuildArtifactsSpy, bsConfig, args._[1], args); + sinon.assert.calledOnce(getConfigPathStub); + sinon.assert.calledOnceWithExactly(sendUsageReportStub, bsConfig, args, constants.usageReportingConstants.GENERATE_DOWNLOADS, Constants.messageTypes.INFO, null); + }) + .catch((error) => { + chai.assert.isNotOk(error, 'Promise error'); + }); + }); + + it("logs and send usage report on rejection", () => { + const generateReport = proxyquire('../../../../bin/commands/generateDownloads', { + '../helpers/utils': { + getConfigPath: getConfigPathStub, + validateBstackJson: validateBstackJsonStub, + setDefaultAuthHash: setDefaultAuthHashStub, + setUsername: setUsernameStub, + setAccessKey: setAccessKeyStub, + setUsageReportingFlag: setUsageReportingFlagStub, + setCypressConfigFilename: setCypressConfigFilenameStub, + sendUsageReport: sendUsageReportStub, + setDefaults: setDefaultsStub, + getErrorCodeFromErr: getErrorCodeFromErrStub + }, + '../helpers/buildArtifacts': { + downloadBuildArtifacts: downloadBuildArtifactsSpy + } + }); + + let err = { message: "Promise error" }; + + validateBstackJsonStub.returns(Promise.reject(err)); + + generateReport(args) + .then(function (_bsConfig) { + sinon.assert.notCalled(downloadBuildArtifactsSpy); + sinon.assert.notCalled(getConfigPathStub); + }) + .catch((_error) => { + sinon.assert.calledWith(setUsageReportingFlagStub, null, args.disableUsageReporting); + sinon.assert.calledWith(sendUsageReportStub, null, args, err.message, Constants.messageTypes.ERROR, "random-error"); + }); + }); + }); +}); diff --git a/test/unit/bin/commands/runs.js b/test/unit/bin/commands/runs.js index 41f47f9d..e59af93c 100644 --- a/test/unit/bin/commands/runs.js +++ b/test/unit/bin/commands/runs.js @@ -5,8 +5,6 @@ const chai = require("chai"), const Constants = require("../../../../bin/helpers/constants"), logger = require("../../../../bin/helpers/logger").winstonLogger, testObjects = require("../../support/fixtures/testObjects"); -const { initTimeComponents, markBlockStart, markBlockEnd } = require("../../../../bin/helpers/timeComponents"); -const { setHeaded, setupLocalTesting, stopLocalBinary, setUserSpecs, setLocalConfigFile } = require("../../../../bin/helpers/utils"); const proxyquire = require("proxyquire").noCallThru(); @@ -104,6 +102,7 @@ describe("runs", () => { setLocalIdentifierStub = sandbox.stub(); setHeadedStub = sandbox.stub(); setNoWrapStub = sandbox.stub(); + setOtherConfigsStub = sandbox.stub(); deleteResultsStub = sandbox.stub(); setDefaultsStub = sandbox.stub(); setLocalModeStub = sandbox.stub(); @@ -137,6 +136,7 @@ describe("runs", () => { setLocalIdentifier: setLocalIdentifierStub, setHeaded: setHeadedStub, setNoWrap: setNoWrapStub, + setOtherConfigs: setOtherConfigsStub, deleteResults: deleteResultsStub, setDefaults: setDefaultsStub, setupLocalTesting: setupLocalTestingStub, @@ -176,6 +176,7 @@ describe("runs", () => { sinon.assert.calledOnce(setLocalConfigFileStub); sinon.assert.calledOnce(setHeadedStub); sinon.assert.calledOnce(setNoWrapStub); + sinon.assert.calledOnce(setOtherConfigsStub); sinon.assert.calledOnce(capabilityValidatorStub); sinon.assert.calledOnce(getErrorCodeFromMsgStub); sinon.assert.calledOnce(setLocalIdentifierStub); @@ -222,6 +223,7 @@ describe("runs", () => { setLocalIdentifierStub = sandbox.stub(); setHeadedStub = sandbox.stub(); setNoWrapStub = sandbox.stub(); + setOtherConfigsStub = sandbox.stub(); deleteResultsStub = sandbox.stub(); getNumberOfSpecFilesStub = sandbox.stub().returns([]); setDefaultsStub = sandbox.stub(); @@ -258,6 +260,7 @@ describe("runs", () => { setLocalIdentifier: setLocalIdentifierStub, setHeaded: setHeadedStub, setNoWrap: setNoWrapStub, + setOtherConfigs: setOtherConfigsStub, deleteResults: deleteResultsStub, setDefaults: setDefaultsStub, getNumberOfSpecFiles: getNumberOfSpecFilesStub, @@ -304,6 +307,7 @@ describe("runs", () => { sinon.assert.calledOnce(setLocalIdentifierStub); sinon.assert.calledOnce(setHeadedStub); sinon.assert.calledOnce(setNoWrapStub); + sinon.assert.calledOnce(setOtherConfigsStub); sinon.assert.calledOnce(validateBstackJsonStub); sinon.assert.calledOnce(capabilityValidatorStub); sinon.assert.calledOnce(archiverStub); @@ -355,6 +359,7 @@ describe("runs", () => { setLocalIdentifierStub = sandbox.stub(); setHeadedStub = sandbox.stub(); setNoWrapStub = sandbox.stub(); + setOtherConfigsStub = sandbox.stub(); deleteResultsStub = sandbox.stub(); getNumberOfSpecFilesStub = sandbox.stub().returns([]); setDefaultsStub = sandbox.stub(); @@ -392,6 +397,7 @@ describe("runs", () => { setLocalIdentifier: setLocalIdentifierStub, setHeaded: setHeadedStub, setNoWrap: setNoWrapStub, + setOtherConfigs: setOtherConfigsStub, deleteResults: deleteResultsStub, getNumberOfSpecFiles: getNumberOfSpecFilesStub, setDefaults: setDefaultsStub, @@ -437,6 +443,7 @@ describe("runs", () => { sinon.assert.calledOnce(setLocalIdentifierStub); sinon.assert.calledOnce(setHeadedStub); sinon.assert.calledOnce(setNoWrapStub); + sinon.assert.calledOnce(setOtherConfigsStub); sinon.assert.calledOnce(validateBstackJsonStub); sinon.assert.calledOnce(capabilityValidatorStub); sinon.assert.calledOnce(archiverStub); @@ -492,6 +499,7 @@ describe("runs", () => { setLocalIdentifierStub = sandbox.stub(); setHeadedStub = sandbox.stub(); setNoWrapStub = sandbox.stub(); + setOtherConfigsStub = sandbox.stub(); deleteResultsStub = sandbox.stub(); getNumberOfSpecFilesStub = sandbox.stub().returns([]); setDefaultsStub = sandbox.stub(); @@ -530,6 +538,7 @@ describe("runs", () => { setLocalIdentifier: setLocalIdentifierStub, setHeaded: setHeadedStub, setNoWrap: setNoWrapStub, + setOtherConfigs: setOtherConfigsStub, deleteResults: deleteResultsStub, getNumberOfSpecFiles: getNumberOfSpecFilesStub, setDefaults: setDefaultsStub, @@ -586,6 +595,7 @@ describe("runs", () => { sinon.assert.calledOnce(setLocalIdentifierStub); sinon.assert.calledOnce(setHeadedStub); sinon.assert.calledOnce(setNoWrapStub); + sinon.assert.calledOnce(setOtherConfigsStub); sinon.assert.calledOnce(archiverStub); sinon.assert.calledOnce(setUsageReportingFlagStub); sinon.assert.calledOnce(zipUploadStub); @@ -646,12 +656,15 @@ describe("runs", () => { setLocalIdentifierStub = sandbox.stub(); setHeadedStub = sandbox.stub(); setNoWrapStub = sandbox.stub(); + setOtherConfigsStub = sandbox.stub(); getNumberOfSpecFilesStub = sandbox.stub().returns([]); setLocalConfigFileStub = sandbox.stub(); getTimeComponentsStub = sandbox.stub().returns({}); initTimeComponentsStub = sandbox.stub(); markBlockStartStub = sandbox.stub(); markBlockEndStub = sandbox.stub(); + stopLocalBinaryStub = sandbox.stub(); + nonEmptyArrayStub = sandbox.stub(); }); afterEach(() => { @@ -687,12 +700,15 @@ describe("runs", () => { setLocalIdentifier: setLocalIdentifierStub, setHeaded: setHeadedStub, setNoWrap: setNoWrapStub, + setOtherConfigs: setOtherConfigsStub, exportResults: exportResultsStub, deleteResults: deleteResultsStub, setDefaults: setDefaultsStub, isUndefined: isUndefinedStub, getNumberOfSpecFiles: getNumberOfSpecFilesStub, setLocalConfigFile: setLocalConfigFileStub, + stopLocalBinary: stopLocalBinaryStub, + nonEmptyArray: nonEmptyArrayStub, }, '../helpers/capabilityHelper': { validate: capabilityValidatorStub, @@ -731,6 +747,8 @@ describe("runs", () => { archiverStub.returns(Promise.resolve("Zipping completed")); checkUploadedStub.returns(Promise.resolve({ zipUrlPresent: false })) zipUploadStub.returns(Promise.resolve("zip uploaded")); + stopLocalBinaryStub.returns(Promise.resolve("nothing")); + nonEmptyArrayStub.returns(false); createBuildStub.returns(Promise.resolve({ message: 'Success', build_id: 'random_build_id', dashboard_url: dashboardUrl })); return runs(args) @@ -752,6 +770,7 @@ describe("runs", () => { sinon.assert.calledOnce(setLocalIdentifierStub); sinon.assert.calledOnce(setHeadedStub); sinon.assert.calledOnce(setNoWrapStub); + sinon.assert.calledOnce(setOtherConfigsStub); sinon.assert.calledOnce(archiverStub); sinon.assert.calledOnce(setUsageReportingFlagStub); sinon.assert.calledOnce(zipUploadStub); diff --git a/test/unit/bin/helpers/utils.js b/test/unit/bin/helpers/utils.js index 43a7c97a..b5c2d505 100644 --- a/test/unit/bin/helpers/utils.js +++ b/test/unit/bin/helpers/utils.js @@ -2032,4 +2032,78 @@ describe('utils', () => { }); }); + describe('setOtherConfigs', () => { + it('set reporter arg in run_settings', () => { + let bsConfig = { + run_settings: { + } + }; + let args = { + reporter: "mocha", + 'reporter-options': "random-string" + }; + utils.setOtherConfigs(bsConfig, args); + expect(bsConfig.run_settings.reporter).to.be.eql("mocha"); + }); + + it('set reporter-options arg in run_settings', () => { + let bsConfig = { + run_settings: { + } + }; + let args = { + 'reporterOptions': "random-string" + }; + utils.setOtherConfigs(bsConfig, args); + expect(bsConfig.run_settings.reporter_options).to.be.eql("random-string"); + }); + }); + + describe('getCypressJSON', () => { + let sampleJson = { + a: "b" + }; + + beforeEach(() => { + sinon.stub(fs, 'readFileSync').returns(JSON.stringify(sampleJson)); + }); + + afterEach(() => { + fs.readFileSync.restore(); + }); + + it('return undefined if param not present', () => { + let bsConfig = { + run_settings: { + } + }; + expect(utils.getCypressJSON(bsConfig)).to.be.eql(undefined); + }); + + it('read file and return json if param present', () => { + let bsConfig = { + run_settings: { + cypress_config_file: './cypress.json' + } + }; + + expect(utils.getCypressJSON(bsConfig)).to.be.eql(sampleJson); + }); + }); + + describe('nonEmptyArray', () => { + it('return true if non empty array', () => { + expect(utils.nonEmptyArray([1, 2, 3])).to.be.eql(true); + expect(utils.nonEmptyArray(["abc"])).to.be.eql(true); + }); + + it('return false if empty array', () => { + expect(utils.nonEmptyArray([])).to.be.eql(false); + }); + + it('return false if null', () => { + expect(utils.nonEmptyArray(null)).to.be.eql(false); + }); + }); + }); diff --git a/test/unit/support/fixtures/testObjects.js b/test/unit/support/fixtures/testObjects.js index 9c188649..b864ce97 100644 --- a/test/unit/support/fixtures/testObjects.js +++ b/test/unit/support/fixtures/testObjects.js @@ -52,6 +52,16 @@ const generateReportInputArgs = { $0: "browserstack-cypress", }; +const generateDownloadsInputArgs = { + _: ["generate-downloads", "f3c94f7203792d03a75be3912d19354fe0961e53"], + cf: "browserstack.json", + "config-file": "browserstack.json", + configFile: "browserstack.json", + "disable-usage-reporting": undefined, + disableUsageReporting: undefined, + $0: "browserstack-cypress", +}; + const buildInfoSampleBody = { build_id: "random_hashed_id", framework: "cypress", @@ -146,4 +156,5 @@ module.exports = Object.freeze({ sampleCapsData, runSampleArgs, generateReportInputArgs, + generateDownloadsInputArgs, });