diff --git a/bin/commands/runs.js b/bin/commands/runs.js index 0d9a0e5c..b261ba0f 100644 --- a/bin/commands/runs.js +++ b/bin/commands/runs.js @@ -7,7 +7,8 @@ const archiver = require("../helpers/archiver"), capabilityHelper = require("../helpers/capabilityHelper"), Constants = require("../helpers/constants"), utils = require("../helpers/utils"), - fileHelpers = require("../helpers/fileHelpers"); + fileHelpers = require("../helpers/fileHelpers"), + syncRunner = require("../helpers/syncRunner"); module.exports = function run(args) { let bsConfigPath = utils.getConfigPath(args.cf); @@ -46,7 +47,6 @@ module.exports = function run(args) { // Validate browserstack.json values and parallels specified via arguments return capabilityHelper.validate(bsConfig, args).then(function (validated) { - logger.info(validated); // accept the number of parallels utils.setParallels(bsConfig, args); @@ -56,20 +56,29 @@ module.exports = function run(args) { // Uploaded zip file return zipUploader.zipUpload(bsConfig, config.fileName).then(function (zip) { - // Create build return build.createBuild(bsConfig, zip).then(function (data) { let message = `${data.message}! ${Constants.userMessages.BUILD_CREATED} with build id: ${data.build_id}`; - let dashboardLink = `${Constants.userMessages.VISIT_DASHBOARD} ${config.dashboardUrl}${data.build_id}`; + let dashboardLink = `${Constants.userMessages.VISIT_DASHBOARD} ${data.dashboard_url}`; utils.exportResults(data.build_id, `${config.dashboardUrl}${data.build_id}`); if ((utils.isUndefined(bsConfig.run_settings.parallels) && utils.isUndefined(args.parallels)) || (!utils.isUndefined(bsConfig.run_settings.parallels) && bsConfig.run_settings.parallels == Constants.cliMessages.RUN.DEFAULT_PARALLEL_MESSAGE)) { logger.warn(Constants.userMessages.NO_PARALLELS); } - if (!args.disableNpmWarning && bsConfig.run_settings.npm_dependencies && Object.keys(bsConfig.run_settings.npm_dependencies).length <= 0) logger.warn(Constants.userMessages.NO_NPM_DEPENDENCIES); + if (!args.disableNpmWarning && bsConfig.run_settings.npm_dependencies && Object.keys(bsConfig.run_settings.npm_dependencies).length <= 0) { + logger.warn(Constants.userMessages.NO_NPM_DEPENDENCIES); + logger.warn(Constants.userMessages.NO_NPM_DEPENDENCIES_READ_MORE); + } + if (args.sync) { + syncRunner.pollBuildStatus(bsConfig, data).then((exitCode) => { + utils.sendUsageReport(bsConfig, args, `${message}\n${dashboardLink}`, Constants.messageTypes.SUCCESS, null); + utils.handleSyncExit(exitCode, data.dashboard_url) + }); + } logger.info(message); logger.info(dashboardLink); + if(!args.sync) logger.info(Constants.userMessages.EXIT_SYNC_CLI_MESSAGE.replace("",data.build_id)); utils.sendUsageReport(bsConfig, args, `${message}\n${dashboardLink}`, Constants.messageTypes.SUCCESS, null); return; }).catch(function (err) { @@ -81,9 +90,8 @@ module.exports = function run(args) { // Zip Upload failed logger.error(err); logger.error(Constants.userMessages.ZIP_UPLOAD_FAILED); - utils.sendUsageReport(bsConfig, args, `${err}\n${Constants.userMessages.ZIP_UPLOAD_FAILED}`, Constants.messageTypes.ERROR, 'zip_upload_failed'); - }).finally(function () { fileHelpers.deleteZip(); + utils.sendUsageReport(bsConfig, args, `${err}\n${Constants.userMessages.ZIP_UPLOAD_FAILED}`, Constants.messageTypes.ERROR, 'zip_upload_failed'); }); }).catch(function (err) { // Zipping failed diff --git a/bin/helpers/archiver.js b/bin/helpers/archiver.js index 4ffcccd1..61364963 100644 --- a/bin/helpers/archiver.js +++ b/bin/helpers/archiver.js @@ -13,6 +13,8 @@ const archiveSpecs = (runSettings, filePath, excludeFiles) => { var cypressFolderPath = path.dirname(runSettings.cypressConfigFilePath); + logger.info(`Creating tests.zip with files in ${cypressFolderPath}`); + var archive = archiver('zip', { zlib: { level: 9 } // Sets the compression level. }); diff --git a/bin/helpers/capabilityHelper.js b/bin/helpers/capabilityHelper.js index bb7a3268..c99851da 100644 --- a/bin/helpers/capabilityHelper.js +++ b/bin/helpers/capabilityHelper.js @@ -20,17 +20,20 @@ const caps = (bsConfig, zip) => { // Browser list let osBrowserArray = []; + let browsersList = []; if (bsConfig.browsers) { bsConfig.browsers.forEach((element) => { osBrowser = element.os + "-" + element.browser; + osAndBrowser = element.os + " / " + Utils.capitalizeFirstLetter(element.browser); element.versions.forEach((version) => { osBrowserArray.push(osBrowser + version); + browsersList.push(`${osAndBrowser} (${version})`); }); }); } obj.devices = osBrowserArray; if (obj.devices.length == 0) reject(Constants.validationMessages.EMPTY_BROWSER_LIST); - logger.info(`Browser list: ${osBrowserArray.toString()}`); + logger.info(`Browsers list: ${browsersList.join(", ")}`); // Test suite if (zip.zip_url && zip.zip_url.split("://")[1].length !== 0) { @@ -38,12 +41,11 @@ const caps = (bsConfig, zip) => { } else { reject("Test suite is empty"); } - logger.info(`Test suite: bs://${obj.test_suite}`); // Local obj.local = false; if (bsConfig.connection_settings && bsConfig.connection_settings.local === true) obj.local = true; - logger.info(`Local is set to: ${obj.local}`); + logger.info(`Local is set to: ${obj.local} (${obj.local ? Constants.userMessages.LOCAL_TRUE : Constants.userMessages.LOCAL_FALSE})`); // Local Identifier obj.localIdentifier = null; @@ -100,6 +102,7 @@ const caps = (bsConfig, zip) => { const validate = (bsConfig, args) => { return new Promise(function (resolve, reject) { + logger.info(Constants.userMessages.VALIDATING_CONFIG); if (!bsConfig) reject(Constants.validationMessages.EMPTY_BROWSERSTACK_JSON); if (!bsConfig.auth) reject(Constants.validationMessages.INCORRECT_AUTH_PARAMS); diff --git a/bin/helpers/config.js b/bin/helpers/config.js index da88c3ab..3f6daeed 100644 --- a/bin/helpers/config.js +++ b/bin/helpers/config.js @@ -16,5 +16,7 @@ config.cypress_v1 = `${config.rails_host}/automate/cypress/v1`; config.buildUrl = `${config.cypress_v1}/builds/`; config.buildStopUrl = `${config.cypress_v1}/builds/stop/`; config.fileName = "tests.zip"; +config.retries = 5; +config.networkErrorExitCode = 2; module.exports = config; diff --git a/bin/helpers/constants.js b/bin/helpers/constants.js index 0f89948a..8b6b4c4c 100644 --- a/bin/helpers/constants.js +++ b/bin/helpers/constants.js @@ -1,21 +1,41 @@ +let config = require("./config"); + +const syncCLI = { + FAILED_SPEC_DETAILS_COL_HEADER: ['Spec', 'Status', 'Browser', 'BrowserStack Session ID'], + LOGS: { + INIT_LOG: "All tests:" + }, + INITIAL_DELAY_MULTIPLIER: 10 +}; + const userMessages = { - BUILD_FAILED: "Build creation failed.", - BUILD_CREATED: "Build created", - BUILD_INFO_FAILED: "Failed to get build info.", - BUILD_STOP_FAILED: "Failed to stop build.", - ZIP_UPLOADER_NOT_REACHABLE: "Could not reach to zip uploader.", - ZIP_UPLOAD_FAILED: "Zip Upload failed.", - CONFIG_FILE_CREATED: "BrowserStack Config File created, you can now run browserstack-cypress --config-file run", - CONFIG_FILE_EXISTS: "File already exists, delete the browserstack.json file manually. skipping...", - DIR_NOT_FOUND: "Given path does not exist. Failed to create browserstack.json in %s", - ZIP_DELETE_FAILED: "Could not delete local file.", - ZIP_DELETED: "Zip file deleted successfully.", - API_DEPRECATED: "This version of API is deprecated, please use latest version of API.", - FAILED_TO_ZIP: "Failed to zip files.", - VISIT_DASHBOARD: "Visit the Automate dashboard for test reporting:", - CONFLICTING_INIT_ARGUMENTS: "Conflicting arguments given. You can use --path only with a file name, and not with a file path.", - NO_PARALLELS: "Your tests will run sequentially. Read more about running your tests in parallel here: https://www.browserstack.com/docs/automate/cypress/run-tests-in-parallel", - NO_NPM_DEPENDENCIES: "No npm dependencies specified. Read more here: https://www.browserstack.com/docs/automate/cypress/npm-packages. You can suppress this warning by using --disable-npm-warning flag." + BUILD_FAILED: "Build creation failed.", + BUILD_CREATED: "Build created", + BUILD_INFO_FAILED: "Failed to get build info.", + BUILD_STOP_FAILED: "Failed to stop build.", + BUILD_REPORT_MESSAGE: "See the entire build report here:", + ZIP_UPLOADER_NOT_REACHABLE: "Could not reach to zip uploader.", + ZIP_UPLOAD_FAILED: "Zip Upload failed.", + CONFIG_FILE_CREATED: "BrowserStack Config File created, you can now run browserstack-cypress --config-file run", + CONFIG_FILE_EXISTS: "File already exists, delete the browserstack.json file manually. skipping...", + DIR_NOT_FOUND: "Given path does not exist. Failed to create browserstack.json in %s", + ZIP_DELETE_FAILED: "Could not delete tests.zip successfully.", + ZIP_DELETED: "Deleted tests.zip successfully.", + API_DEPRECATED: "This version of API is deprecated, please use latest version of API.", + FAILED_TO_ZIP: "Failed to zip files.", + VISIT_DASHBOARD: "Visit the Automate dashboard for real-time test reporting:", + CONFLICTING_INIT_ARGUMENTS: "Conflicting arguments given. You can use --path only with a file name, and not with a file path.", + NO_PARALLELS: "Your specs will run sequentially on a single machine. Read more about running your specs in parallel here: https://www.browserstack.com/docs/automate/cypress/run-tests-in-parallel", + NO_NPM_DEPENDENCIES: "No npm dependencies specified - your specs might fail if they need any packages to be installed before running.", + NO_NPM_DEPENDENCIES_READ_MORE: "Read more about npm dependencies here: https://www.browserstack.com/docs/automate/cypress/npm-packages. You can suppress this warning by using --disable-npm-warning flag.", + VALIDATING_CONFIG: "Validating the config", + UPLOADING_TESTS: "Uploading the tests to BrowserStack", + LOCAL_TRUE: "you will now be able to test localhost / private URLs", + LOCAL_FALSE: "you won't be able to test localhost / private URLs", + 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 command now.", + FATAL_NETWORK_ERROR: `fatal: unable to access '${config.buildUrl}': Could not resolve host: ${config.rails_host}`, + RETRY_LIMIT_EXCEEDED: `Max retries exceeded trying to connect to the host (retries: ${config.retries})`, + CHECK_DASHBOARD_AT: "Please check the build status at: " }; const validationMessages = { @@ -39,44 +59,46 @@ const validationMessages = { }; const cliMessages = { - VERSION: { - INFO: "shows version information", - HELP: "Specify --help for available options", - DEMAND: "Requires init, run or poll argument" - }, - INIT: { - INFO: "create a browserstack.json file in the folder specified with the default configuration options.", - DESC: "Init in a specified folder" - }, - BUILD: { - 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", - DEFAULT_PARALLEL_MESSAGE: "Here goes the number of parallels you want to run", - SPECS_DESCRIPTION: 'Specify the spec files to run', - ENV_DESCRIPTION: "Specify the environment variables for your spec files" - }, - COMMON: { - DISABLE_USAGE_REPORTING: "Disable usage reporting", - USERNAME: "Your BrowserStack username", - ACCESS_KEY: "Your BrowserStack access key", - NO_NPM_WARNING: "No NPM warning if npm_dependencies is empty" - } -} + VERSION: { + INFO: "shows version information", + HELP: "Specify --help for available options", + DEMAND: "Requires init, run or poll argument", + }, + INIT: { + INFO: "create a browserstack.json file in the folder specified with the default configuration options.", + DESC: "Init in a specified folder", + }, + BUILD: { + 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", + DEFAULT_PARALLEL_MESSAGE: "Here goes the number of parallels you want to run", + SPECS_DESCRIPTION: "Specify the spec files to run", + ENV_DESCRIPTION: "Specify the environment variables for your spec files", + SYNC_DESCRIPTION: "Makes the run command in sync", + BUILD_REPORT_MESSAGE: "See the entire build report here", + }, + COMMON: { + DISABLE_USAGE_REPORTING: "Disable usage reporting", + USERNAME: "Your BrowserStack username", + ACCESS_KEY: "Your BrowserStack access key", + NO_NPM_WARNING: "No NPM warning if npm_dependencies is empty", + }, +}; const messageTypes = { SUCCESS: "success", @@ -92,6 +114,7 @@ const allowedFileTypes = ['js', 'json', 'txt', 'ts', 'feature', 'features', 'pdf const filesToIgnoreWhileUploading = ['node_modules/**', 'package-lock.json', 'package.json', 'browserstack-package.json', 'tests.zip', 'cypress.json'] module.exports = Object.freeze({ + syncCLI, userMessages, cliMessages, validationMessages, diff --git a/bin/helpers/fileHelpers.js b/bin/helpers/fileHelpers.js index cd01f3af..dd8f0383 100644 --- a/bin/helpers/fileHelpers.js +++ b/bin/helpers/fileHelpers.js @@ -2,17 +2,17 @@ const fs = require('fs-extra'), path = require('path'); -const logger = require("./logger").winstonLogger, - Constants = require("../helpers/constants"), - config = require("../helpers/config"); +const logger = require('./logger').winstonLogger, + Constants = require('../helpers/constants'), + config = require('../helpers/config'); -exports.write = function(f, message, args, cb) { +exports.write = function (f, message, args, cb) { message = message || 'Creating'; - fs.writeFile(f.path, f.file, function() { - logger.info(message + " file: " + f.path); - cb && cb(args) + fs.writeFile(f.path, f.file, function () { + logger.info(message + ' file: ' + f.path); + cb && cb(args); }); -} +}; exports.fileExists = function (filePath, cb) { fs.access(filePath, fs.F_OK, (err) => { @@ -25,16 +25,15 @@ exports.fileExists = function (filePath, cb) { }; exports.deleteZip = () => { - return fs.unlink(config.fileName, function (err) { - if (err) { - logger.info(Constants.userMessages.ZIP_DELETE_FAILED); - return 1; - } else { - logger.info(Constants.userMessages.ZIP_DELETED); - return 0; - } - }); -} + try { + fs.unlinkSync(config.fileName); + logger.info(Constants.userMessages.ZIP_DELETED); + return 0; + } catch (err) { + logger.info(Constants.userMessages.ZIP_DELETE_FAILED); + return 1; + } +}; exports.dirExists = function (filePath, cb) { let exists = false; @@ -42,4 +41,4 @@ exports.dirExists = function (filePath, cb) { exists = true; } cb && cb(exists); -} +}; diff --git a/bin/helpers/logger.js b/bin/helpers/logger.js index 5188f358..f6fafa41 100644 --- a/bin/helpers/logger.js +++ b/bin/helpers/logger.js @@ -21,6 +21,16 @@ const winstonLoggerParams = { ], }; +const winstonSyncCliLoggerParams = { + transports: [ + new (winston.transports.Console)({ + formatter: (options) => { + return (options.message ? options.message : ''); + } + }), + ] +} + const winstonFileLoggerParams = { transports: [ new winston.transports.File({ @@ -31,3 +41,4 @@ const winstonFileLoggerParams = { exports.winstonLogger = new winston.Logger(winstonLoggerParams); exports.fileLogger = new winston.Logger(winstonFileLoggerParams); +exports.syncCliLogger = new winston.Logger(winstonSyncCliLoggerParams); diff --git a/bin/helpers/sync/failedSpecsDetails.js b/bin/helpers/sync/failedSpecsDetails.js new file mode 100644 index 00000000..4710eea8 --- /dev/null +++ b/bin/helpers/sync/failedSpecsDetails.js @@ -0,0 +1,82 @@ +const tablePrinter = require('table'), // { table, getBorderCharacters } + chalk = require('chalk'), + Constants = require("../constants"), + logger = require("../logger").syncCliLogger, + config = require("../config"); + +/** + * + * @param {Array.<{specName: string, status: string, combination: string, sessionId: string}>} data + * @returns {Promise.resolve || Promise.reject} + */ +// Example: +// [ +// {specName: 'spec1.failed.js', status: 'Failed', combination: 'Win 10 / Chrome 78', sessionId: '3d3rdf3r...'}, +// {specName: 'spec2.name.js', status: 'Skipped', combination: 'Win 10 / Chrome 78', sessionId: '3d3rdf3r...'}, +// {specName: 'spec3.network.js', status: 'Failed', combination: 'Win 10 / Chrome 78', sessionId: '3d3rdf3r...'}, +// {specName: 'spec6.utils.js', status: 'Failed', combination: 'Win 10 / Chrome 78', sessionId: '3d3rdf3r...'}, +// {specName: 'spec8.alias.js', status: 'Skipped', combination: 'Win 10 / Chrome 78', sessionId: '3d3rdf3r...'} +// ] +// +let failedSpecsDetails = (data) => { + return new Promise((resolve, reject) => { + if (!data.exitCode) data.exitCode = 0; + + if (data.specs.length === 0) resolve(data); // return if no failed/skipped tests. + + let failedSpecs = false; + let specResultHeader = Constants.syncCLI.FAILED_SPEC_DETAILS_COL_HEADER.map((col) => { + return chalk.blueBright(col); + }); + + let specData = [specResultHeader]; // 2-D array + + data.specs.forEach((spec) => { + if (spec.status.toLowerCase() === 'passed') { + return; + } + if (spec.status && spec.status.toLowerCase() === 'failed' && !failedSpecs) + failedSpecs = true; + + let specStatus = + spec.status && spec.status.toLowerCase() === 'failed' + ? chalk.red(spec.status) + : chalk.yellow(spec.status); + specData.push([ + spec.specName, + specStatus, + spec.combination, + spec.sessionId, + ]); + }); + + let tableConfig = { + border: tablePrinter.getBorderCharacters('ramac'), + columns: { + 0: { alignment: 'left' }, + 1: { alignment: 'left' }, + 2: { alignment: 'left' }, + 3: { alignment: 'left' }, + }, + /** + * @typedef {function} drawHorizontalLine + * @param {number} index + * @param {number} size + * @return {boolean} + */ + drawHorizontalLine: (index, size) => { + return (index === 0 || index === 1 || index === size); + } + } + + let result = tablePrinter.table(specData, tableConfig); + + logger.info('\nFailed / skipped test report:'); + logger.info(result); + + if (failedSpecs && data.exitCode !== config.networkErrorExitCode) data.exitCode = 1 ; // specs failed, send exitCode as 1 + resolve(data); // No Specs failed, maybe skipped, but not failed, send exitCode as 0 + }); +} + +exports.failedSpecsDetails = failedSpecsDetails; diff --git a/bin/helpers/sync/specsSummary.js b/bin/helpers/sync/specsSummary.js new file mode 100644 index 00000000..bdab4fa5 --- /dev/null +++ b/bin/helpers/sync/specsSummary.js @@ -0,0 +1,54 @@ +const logger = require("../logger").syncCliLogger; + +/** + * + * @param {Array.<{specName: string, status: string, combination: string, sessionId: string}>} data + * @param {String} time + * @param {Number} machines + * @returns {Promise.resolve || Promise.reject} + */ +// Example: +// [ +// {specName: 'spec1.failed.js', status: 'Failed', combination: 'Win 10 / Chrome 78', sessionId: '3d3rdf3r...'}, +// {specName: 'spec2.name.js', status: 'Skipped', combination: 'Win 10 / Chrome 78', sessionId: '3d3rdf3r...'}, +// {specName: 'spec3.network.js', status: 'Failed', combination: 'Win 10 / Chrome 78', sessionId: '3d3rdf3r...'}, +// {specName: 'spec6.utils.js', status: 'Failed', combination: 'Win 10 / Chrome 78', sessionId: '3d3rdf3r...'}, +// {specName: 'spec8.alias.js', status: 'Skipped', combination: 'Win 10 / Chrome 78', sessionId: '3d3rdf3r...'} +// ] +// +let printSpecsRunSummary = (data, machines) => { + return new Promise((resolve, _reject) => { + let summary = { + total: 0, + failed: 0, + passed: 0, + skipped: 0 + }; + + data.specs.forEach((spec) => { + specSummaryCount(summary, spec.status.toLowerCase()); + }); + + logger.info(`Total tests: ${summary.total}, passed: ${summary.passed}, failed: ${summary.failed}, skipped: ${summary.skipped}`); + logger.info(`Done in ${data.duration/1000} seconds using ${machines} machines\n`); + + resolve(data.exitCode); + }) +}; + +let specSummaryCount = (summary, status) => { + switch (status) { + case 'failed': + summary.failed++; + break; + case 'skipped': + summary.skipped++; + break; + case 'passed': + summary.passed++; + break; + } + summary.total++; +}; + +exports.printSpecsRunSummary = printSpecsRunSummary; diff --git a/bin/helpers/sync/syncSpecsLogs.js b/bin/helpers/sync/syncSpecsLogs.js new file mode 100644 index 00000000..558ab941 --- /dev/null +++ b/bin/helpers/sync/syncSpecsLogs.js @@ -0,0 +1,182 @@ +"use strict"; +const request = require("request"), + config = require("../config"), + utils = require("../utils"), + logger = require("../logger").syncCliLogger, + async = require('async'), + Constants = require("../constants"), + tableStream = require('table').createStream, + chalk = require('chalk'); + +let whileLoop = true, whileTries = config.retries, options, timeout = 3000, n = 10, tableConfig, stream, endTime, startTime = Date.now(); +let specSummary = { + "specs": [], + "duration": null +} + + +let getOptions = (auth, build_id) => { + return { + url: `${config.buildUrl}${build_id}`, + auth: { + user: auth.username, + password: auth.access_key + }, + timeout: 10000, + headers: { + "Content-Type": "application/json", + "User-Agent": utils.getUserAgent() + } + }; +} + +let getTableConfig = () => { + return { + border: getBorderConfig(), + columns: { + 1: {alignment: 'center', width: 1}, + 2: {alignment: 'left', width: 50} + }, + columnDefault: { + width: 25, + }, + columnCount: 3, + }; +} + +let getBorderConfig = () => { + return { + topBody: ``, + topJoin: ``, + topLeft: ``, + topRight: ``, + + bottomBody: ``, + bottomJoin: ``, + bottomLeft: ``, + bottomRight: ``, + + bodyLeft: ``, + bodyRight: ``, + bodyJoin: ``, + + joinBody: ``, + joinLeft: ``, + joinRight: ``, + joinJoin: `` + } +} + +let printSpecsStatus = (bsConfig, buildDetails) => { + return new Promise((resolve, reject) => { + options = getOptions(bsConfig.auth, buildDetails.build_id) + tableConfig = getTableConfig(); + stream = tableStream(tableConfig); + + async.whilst( + function() { // condition for loop + return whileLoop; + }, + function(callback) { // actual loop + whileProcess(callback) + }, + function(err, result) { // when loop ends + logger.info("\n--------------------------------------------------------------------------------") + specSummary.duration = endTime - startTime + resolve(specSummary) + } + ); + }); +}; + +let whileProcess = (whilstCallback) => { + request.post(options, function(error, response, body) { + if (error) { + whileTries -= 1; + if (whileTries === 0) { + whileLoop = false; + endTime = Date.now(); + specSummary.exitCode = config.networkErrorExitCode; + return whilstCallback({ status: 504, message: "Tries limit reached" }); //Gateway Timeout + } else { + n = 2 + return setTimeout(whilstCallback, timeout * n, null); + } + } + + whileTries = config.retries; // reset to default after every successful request + + switch (response.statusCode) { + case 202: // get data here and print it + n = 2 + showSpecsStatus(body); + return setTimeout(whilstCallback, timeout * n, null); + case 204: // No data available, wait for some time and ask again + n = 1 + return setTimeout(whilstCallback, timeout * n, null); + case 200: // Build is completed. + whileLoop = false; + endTime = Date.now(); + showSpecsStatus(body); + return whilstCallback(null, body); + default: + whileLoop = false; + return whilstCallback({ status: response.statusCode, message: body }); + } + }); +} + +let showSpecsStatus = (data) => { + let specData = JSON.parse(data); + specData.forEach(specDetails => { + if (specDetails == "created") { + printInitialLog(); + } else { + printSpecData(JSON.parse(specDetails)) + } + }); +} + +let printInitialLog = () => { + logger.info(`\n${Constants.syncCLI.LOGS.INIT_LOG}`) + logger.info("--------------------------------------------------------------------------------") + n = Constants.syncCLI.INITIAL_DELAY_MULTIPLIER +} + +let printSpecData = (data) => { + let combination = getCombinationName(data["spec"]); + let status = getStatus(data["spec"]["status"]); + writeToTable(combination, data["path"], status) + addSpecToSummary(data["path"], data["spec"]["status"], combination, data["session_id"]) +} + +let writeToTable = (combination, specName, status) => { + stream.write([combination , ":", `${specName} ${status}`]); +} + +let addSpecToSummary = (specName, status, combination, session_id) => { + // Format for part 3: {specName: 'spec1.failed.js', status: 'Failed', combination: 'Win 10 / Chrome 78', sessionId: '3d3rdf3r...'}, + specSummary["specs"].push({ + "specName": specName, + "status": status, + "combination": combination, + "sessionId": session_id + }) +} + +let getCombinationName = (spec) => { + return `${utils.capitalizeFirstLetter(spec['browser'])} ${spec['browserVersion']} (${spec['os']} ${spec['osVersion']})`; +} + +let getStatus = (status) => { + switch(status) { + case "passed": + return chalk.green("✔"); + case "failed": + return chalk.red("✘"); + default: + return chalk.blue(`[${status}]`); + } +} + +exports.printSpecsStatus = printSpecsStatus; diff --git a/bin/helpers/syncRunner.js b/bin/helpers/syncRunner.js new file mode 100644 index 00000000..b98eeca2 --- /dev/null +++ b/bin/helpers/syncRunner.js @@ -0,0 +1,18 @@ +'use strict'; +const syncSpecsLogs = require('./sync/syncSpecsLogs'), + specDetails = require('./sync/failedSpecsDetails'), + specsSummary = require('./sync/specsSummary'); + +exports.pollBuildStatus = (bsConfig, buildDetails) => { + return new Promise((resolve, reject) => { + syncSpecsLogs.printSpecsStatus(bsConfig, buildDetails).then((data) => { + return specDetails.failedSpecsDetails(data); + }).then((data) => { + return specsSummary.printSpecsRunSummary(data, buildDetails.machines); + }).then((successExitCode) => { + resolve(successExitCode); // exit code 0 + }).catch((nonZeroExitCode) => { + resolve(nonZeroExitCode); // exit code 1 + }) + }); +}; diff --git a/bin/helpers/utils.js b/bin/helpers/utils.js index 66df447c..95ffbcae 100644 --- a/bin/helpers/utils.js +++ b/bin/helpers/utils.js @@ -5,7 +5,10 @@ const fs = require("fs"); const usageReporting = require("./usageReporting"), logger = require("./logger").winstonLogger, - Constants = require("./constants"); + Constants = require("./constants"), + chalk = require('chalk'), + syncCliLogger = require("../helpers/logger").syncCliLogger, + config = require("../helpers/config"); exports.validateBstackJson = (bsConfigPath) => { return new Promise(function (resolve, reject) { @@ -314,3 +317,24 @@ exports.setLocalIdentifier = (bsConfig) => { ); } }; + +exports.capitalizeFirstLetter = (stringToCapitalize) => { + return stringToCapitalize && (stringToCapitalize[0].toUpperCase() + stringToCapitalize.slice(1)); +}; + +exports.handleSyncExit = (exitCode, dashboard_url) => { + if (exitCode === config.networkErrorExitCode) { + syncCliLogger.info(this.getNetworkErrorMessage(dashboard_url)); + } else { + syncCliLogger.info(Constants.userMessages.BUILD_REPORT_MESSAGE); + syncCliLogger.info(dashboard_url); + } + process.exit(exitCode); +} + +exports.getNetworkErrorMessage = (dashboard_url) => { + let message = Constants.userMessages.FATAL_NETWORK_ERROR + '\n' + + Constants.userMessages.RETRY_LIMIT_EXCEEDED + '\n' + + Constants.userMessages.CHECK_DASHBOARD_AT + dashboard_url + return chalk.red(message) +} diff --git a/bin/helpers/zipUpload.js b/bin/helpers/zipUpload.js index 4720ae52..654dd481 100644 --- a/bin/helpers/zipUpload.js +++ b/bin/helpers/zipUpload.js @@ -4,10 +4,12 @@ const config = require("./config"), fs = require("fs"), logger = require("./logger").winstonLogger, Constants = require("./constants"), - utils = require("./utils"); + utils = require("./utils"), + fileHelpers = require("./fileHelpers"); const uploadCypressZip = (bsConfig, filePath) => { return new Promise(function (resolve, reject) { + logger.info(Constants.userMessages.UPLOADING_TESTS); let options = { url: config.uploadUrl, auth: { @@ -41,7 +43,8 @@ const uploadCypressZip = (bsConfig, filePath) => { reject(Constants.userMessages.ZIP_UPLOADER_NOT_REACHABLE); } } else { - logger.info(`Zip uploaded with url: ${responseData.zip_url}`); + logger.info(`Uploaded tests successfully (${responseData.zip_url})`); + fileHelpers.deleteZip(); resolve(responseData); } } diff --git a/bin/runner.js b/bin/runner.js index 597eb558..bdc7f5b8 100755 --- a/bin/runner.js +++ b/bin/runner.js @@ -195,6 +195,11 @@ var argv = yargs default: false, description: Constants.cliMessages.COMMON.NO_NPM_WARNING, type: "boolean" + }, + 'sync': { + default: false, + describe: Constants.cliMessages.RUN.SYNC_DESCRIPTION, + type: "boolean" } }) .help('help') diff --git a/package.json b/package.json index 3068c04d..1337e8ae 100644 --- a/package.json +++ b/package.json @@ -13,10 +13,12 @@ }, "dependencies": { "archiver": "^3.1.1", + "chalk": "^4.1.0", "fs-extra": "^8.1.0", "mkdirp": "^1.0.3", "request": "^2.88.0", "requestretry": "^4.1.0", + "table": "^5.4.6", "winston": "^2.3.1", "yargs": "^14.2.2" }, diff --git a/test/unit/bin/commands/runs.js b/test/unit/bin/commands/runs.js index ef8f75ad..d5ab37fd 100644 --- a/test/unit/bin/commands/runs.js +++ b/test/unit/bin/commands/runs.js @@ -481,9 +481,6 @@ describe("runs", () => { messageType, errorCode ); - }) - .finally(function () { - sinon.assert.calledOnce(deleteZipStub); }); }); }); @@ -530,7 +527,7 @@ describe("runs", () => { let messageType = Constants.messageTypes.SUCCESS; let errorCode = null; let message = `Success! ${Constants.userMessages.BUILD_CREATED} with build id: random_build_id`; - let dashboardLink = `${Constants.userMessages.VISIT_DASHBOARD} ${dashboardUrl}random_build_id`; + let dashboardLink = `${Constants.userMessages.VISIT_DASHBOARD} ${dashboardUrl}`; const runs = proxyquire("../../../../bin/commands/runs", { "../helpers/utils": { @@ -578,7 +575,7 @@ describe("runs", () => { ); archiverStub.returns(Promise.resolve("Zipping completed")); zipUploadStub.returns(Promise.resolve("zip uploaded")); - createBuildStub.returns(Promise.resolve({ message: 'Success', build_id: 'random_build_id' })); + createBuildStub.returns(Promise.resolve({ message: 'Success', build_id: 'random_build_id', dashboard_url: dashboardUrl })); return runs(args) .then(function (_bsConfig) { @@ -607,9 +604,6 @@ describe("runs", () => { messageType, errorCode ); - }) - .finally(function () { - sinon.assert.calledOnce(deleteZipStub); }); }); }); diff --git a/test/unit/bin/helpers/fileHelpers.js b/test/unit/bin/helpers/fileHelpers.js index e2ba0b64..17e5493c 100644 --- a/test/unit/bin/helpers/fileHelpers.js +++ b/test/unit/bin/helpers/fileHelpers.js @@ -1,8 +1,10 @@ -const chai = require("chai"), - sinon = require("sinon"), +const chai = require('chai'), + sinon = require('sinon'), expect = chai.expect, assert = chai.assert, - chaiAsPromised = require("chai-as-promised"); + chaiAsPromised = require('chai-as-promised'), + fs = require('fs-extra'), + fileHelpers = require('../../../../bin/helpers/fileHelpers'); const logger = require("../../../../bin/helpers/logger").winstonLogger, proxyquire = require("proxyquire").noCallThru(); @@ -82,30 +84,19 @@ describe("fileHelpers", () => { }); it("deleteZip returns 0 on success", () => { - let unlinkStub = sandbox.stub().yields(); - - const fileHelpers = proxyquire("../../../../bin/helpers/fileHelpers", { - "fs-extra": { - unlink: unlinkStub, - }, - }); + let unlinkStub = sinon.stub(fs, 'unlinkSync').returns(true); let result = fileHelpers.deleteZip(); sinon.assert.calledOnce(unlinkStub); assert.equal(result, 0); + fs.unlinkSync.restore(); }); it("deleteZip returns 1 on failure", () => { - let unlinkStub = sandbox.stub().yields(new Error("random-error")); - - const fileHelpers = proxyquire("../../../../bin/helpers/fileHelpers", { - "fs-extra": { - unlink: unlinkStub, - }, - }); - + let unlinkStub = sinon.stub(fs, 'unlinkSync').yields(new Error("random-error")); let result = fileHelpers.deleteZip(); sinon.assert.calledOnce(unlinkStub); assert.equal(result, 1); + fs.unlinkSync.restore(); }); }); diff --git a/test/unit/bin/helpers/sync/failedSpecDetails.js b/test/unit/bin/helpers/sync/failedSpecDetails.js new file mode 100644 index 00000000..a66f6363 --- /dev/null +++ b/test/unit/bin/helpers/sync/failedSpecDetails.js @@ -0,0 +1,76 @@ +'use strict'; +const chai = require("chai"), + expect = chai.expect, + chaiAsPromised = require("chai-as-promised"); + +const sinon = require("sinon"); +chai.use(chaiAsPromised); +const specDetails = require('../../../../../bin/helpers/sync/failedSpecsDetails'); +var logger = require("../../../../../bin/helpers/logger").syncCliLogger; + +describe("failedSpecsDetails", () => { + var sandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + sandbox.stub(logger, 'info'); + }); + + afterEach(() => { + sandbox.restore(); + }); + + context("data is empty", () => { + let data = { + specs: [], + exitCode: 0 + }; + + it('returns 0 exit code', () => { + return specDetails.failedSpecsDetails(data).then((result) => { + expect(result).to.equal(data); + }); + }); + }); + + context("data does not have failed specs", () => { + let data = { + specs: [{specName: 'spec2.name.js', status: 'Skipped', combination: 'Win 10 / Chrome 78', sessionId: '3d3rdf3r...'}], + exitCode: 0 + }; + + it("returns 0 exit code", () => { + return specDetails.failedSpecsDetails(data).then((result) => { + expect(result).to.equal(data); + }); + }); + }); + + context("data has failed specs", () => { + let data = { + specs: [ {specName: 'spec2.name.js', status: 'Failed', combination: 'Win 10 / Chrome 78', sessionId: '3d3rdf3r...'}, + {specName: 'spec2.name.js', status: 'Passed', combination: 'Win 10 / Chrome 78', sessionId: '3d3rdf3r...'}], + exitCode: 1 + }; + + it("returns 1 exit code", () => { + return specDetails.failedSpecsDetails(data).then((result) => { + expect(result).to.equal(data); + }); + }); + }); + + context("failed because of network issue", () => { + let data = { + specs: [ {specName: 'spec2.name.js', status: 'Failed', combination: 'Win 10 / Chrome 78', sessionId: '3d3rdf3r...'}, + {specName: 'spec2.name.js', status: 'Passed', combination: 'Win 10 / Chrome 78', sessionId: '3d3rdf3r...'}], + exitCode: 2 + }; + + it("returns 2 exit code", () => { + return specDetails.failedSpecsDetails(data).then((result) => { + expect(result).to.equal(data); + }); + }); + }); +}); diff --git a/test/unit/bin/helpers/sync/specSummary.js b/test/unit/bin/helpers/sync/specSummary.js new file mode 100644 index 00000000..4bf7d7a9 --- /dev/null +++ b/test/unit/bin/helpers/sync/specSummary.js @@ -0,0 +1,55 @@ +'use strict'; +const chai = require("chai"), + sinon = require("sinon"), + expect = chai.expect, + chaiAsPromised = require("chai-as-promised"); + +chai.use(chaiAsPromised); +var logger = require("../../../../../bin/helpers/logger").syncCliLogger; +var specSummary = require('../../../../../bin/helpers/sync/specsSummary'); + +describe("printSpecsRunSummary", () => { + context("data is empty", () => { + let data = { specs: [], duration: 6000, exitCode: 0}, machines = 2; + it('returns passed specs data', () => { + return specSummary.printSpecsRunSummary(data, machines).then((specsData) => { + expect(data.exitCode).to.equal(specsData); + }); + }); + }); + + context("request failure", () => { + let data = { specs: [], duration: 6000, exitCode: 2}, machines = 2; + it('returns passed specs data with proper exit code', () => { + return specSummary.printSpecsRunSummary(data, machines).then((specsData) => { + expect(data.exitCode).to.equal(specsData); + }); + }); + }); + + context("with data", () => { + let time = 6000, + machines = 2, + specs = [ + {specName: 'spec2.name.js', status: 'Failed', combination: 'Win 10 / Chrome 78', sessionId: '3d3rdf3r...'}, + {specName: 'spec2.name.js', status: 'Skipped', combination: 'Win 10 / Chrome 78', sessionId: '3d3rdf3r...'}, + {specName: 'spec2.name.js', status: 'Failed', combination: 'Win 10 / Chrome 78', sessionId: '3d3rdf3r...'}, + {specName: 'spec2.name.js', status: 'Passed', combination: 'Win 10 / Chrome 78', sessionId: '3d3rdf3r...'} + ], + data = { + specs: specs, + duration: time, + exitCode: 0 + }; + + it('returns passed specs data', () => { + var loggerInfoSpy = sinon.spy(logger, 'info'); + + specSummary.printSpecsRunSummary(data, machines); + sinon.assert.calledWith(loggerInfoSpy, 'Total tests: 4, passed: 1, failed: 2, skipped: 1'); + sinon.assert.calledWith(loggerInfoSpy, `Done in ${time / 1000} seconds using ${machines} machines\n`); + + loggerInfoSpy.restore(); + }); + }); +}); diff --git a/test/unit/bin/helpers/sync/syncSpecsLogs.js b/test/unit/bin/helpers/sync/syncSpecsLogs.js new file mode 100644 index 00000000..b0c2828f --- /dev/null +++ b/test/unit/bin/helpers/sync/syncSpecsLogs.js @@ -0,0 +1,403 @@ +"use strict"; +const chai = require("chai"), + expect = chai.expect, + rewire = require("rewire"), + chaiAsPromised = require("chai-as-promised"), + chalk = require('chalk'), + request = require("request"); + +const sinon = require("sinon"); + +chai.use(chaiAsPromised); + +var syncSpecsLogs = rewire("../../../../../bin/helpers/sync/syncSpecsLogs.js"); +var logger = require("../../../../../bin/helpers/logger").syncCliLogger; +var Constants = require("../../../../../bin/helpers/constants.js"); +var config = require("../../../../../bin/helpers/config.js"); + +describe("syncSpecsLogs", () => { + var sandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + context("getCombinationName", () => { + const get_path = syncSpecsLogs.__get__("getCombinationName");; + let spec = { + "os": "Windows", + "osVersion": "10", + "browser": "chrome", + "browserVersion": "86" + } + it("returns combination name", () => { + let expectedCombination = `Chrome 86 (Windows 10)`; + expect(get_path(spec)).to.equal(expectedCombination); + }); + }); + + context("getStatus", () => { + const getStatus = syncSpecsLogs.__get__("getStatus");; + + it("returns return ✔ in green when status is passes", () => { + expect(getStatus("passed")).to.equal(chalk.green("✔")); + }); + + it("returns return ✘ in red when status is failed", () => { + expect(getStatus("failed")).to.equal(chalk.red("✘")); + }); + + it("returns return [status] in yellow when status is skipped or ignored (anything else from pass/fail)", () => { + expect(getStatus("skipped")).to.equal(chalk.blue("[skipped]")); + expect(getStatus("ignored")).to.equal(chalk.blue("[ignored]")); + }); + }); + + context("printInitialLog", () => { + const printInitialLog = syncSpecsLogs.__get__("printInitialLog"); + + it("should print inital logs for specs in sync", () => { + + printInitialLog() + + expect(syncSpecsLogs.__get__("n")).to.equal(Constants.syncCLI.INITIAL_DELAY_MULTIPLIER); + expect(syncSpecsLogs.__get__("startTime")).to.not.be.null; + + }); + }); + + context("getOptions", () => { + const getOptions = syncSpecsLogs.__get__("getOptions"); + let auth = {username: "cypress", access_key: "abcd"} + let build_id = "build1" + + it('should return proper request option for polling', () => { + let options = getOptions(auth, build_id); + expect(options.url).to.equal(`${config.buildUrl}${build_id}`); + expect(options.auth.user).to.equal(auth.username); + expect(options.auth.password).to.equal(auth.access_key); + expect(options.headers["Content-Type"]).to.equal("application/json"); + }); + }); + + context("getTableConfig", () => { + const getTableConfig = syncSpecsLogs.__get__("getTableConfig"); + + it('should return proper table config option for spec table', () => { + var getBorderConfigStub = sandbox.stub(); + syncSpecsLogs.__set__('getBorderConfig', getBorderConfigStub); + + let options = getTableConfig(); + expect(options.columnDefault.width).to.equal(25); + expect(options.columns[1].alignment).to.equal('center'); + expect(options.columns[2].alignment).to.equal('left'); + expect(options.columns[1].width).to.equal(1); + expect(options.columns[2].width).to.equal(50); + expect(options.columnCount).to.equal(3); + expect(getBorderConfigStub.calledOnce).to.be.true; + }); + }); + + context("getBorderConfig", () => { + const getBorderConfig = syncSpecsLogs.__get__("getBorderConfig"); + + it('should return proper border option for spec table', () => { + let options = getBorderConfig(); + expect(options.topBody).to.equal(""); + expect(options.bottomBody).to.equal(""); + }); + }); + + context("writeToTable", () => { + const writeToTable = syncSpecsLogs.__get__("writeToTable"); + + it('should print spec details to the table', () => { + const stream = sandbox.stub(); + stream.write = sandbox.stub(); + syncSpecsLogs.__set__('stream', stream); + let combination = "Windows 10", path = "path", status = "passed"; + writeToTable(combination, path, status); + sinon.assert.calledOnceWithExactly(stream.write, [combination , ":", `${path} ${status}`]); + }); + }); + + context("addSpecToSummary", () => { + const addSpecToSummary = syncSpecsLogs.__get__("addSpecToSummary"); + + it('should add spec details to specSummary', () => { + let specSummary = { specs: [] } + syncSpecsLogs.__set__('specSummary', specSummary); + let specName = "spec", status = "status", combination = "combo", session_id = "id"; + addSpecToSummary(specName, status, combination, session_id); + expect(specSummary.specs).deep.to.equal([{"specName": specName, "status": status, "combination": combination, "sessionId": session_id}]) + }); + }); + + context("printSpecData", () => { + const printSpecData = syncSpecsLogs.__get__("printSpecData"); + + it('Should print combination and status to the spec table and add spec details to spec array', () => { + let data = { spec: { status: "passed" }, path: "path", session_id: "id" } + var getCombinationName = sandbox.stub(); + syncSpecsLogs.__set__('getCombinationName', getCombinationName); + var getStatus = sandbox.stub(); + syncSpecsLogs.__set__('getStatus', getStatus); + var writeToTable = sandbox.stub(); + syncSpecsLogs.__set__('writeToTable', writeToTable); + var addSpecToSummary = sandbox.stub(); + syncSpecsLogs.__set__('addSpecToSummary', addSpecToSummary); + + + printSpecData(data); + sinon.assert.calledOnceWithExactly(getCombinationName, data["spec"]); + sinon.assert.calledOnceWithExactly(getStatus, data["spec"]["status"]); + sinon.assert.calledOnce(writeToTable); + sinon.assert.calledOnce(addSpecToSummary); + }); + }); + + + context("showSpecsStatus", () => { + const showSpecsStatus = syncSpecsLogs.__get__("showSpecsStatus"); + + it('should print initial log for running specs when it is the 1st polling response', () => { + let data = JSON.stringify(["created"]) + var printInitialLog = sandbox.stub(); + syncSpecsLogs.__set__('printInitialLog', printInitialLog); + + showSpecsStatus(data); + + expect(printInitialLog.calledOnce).to.be.true; + }); + + it('should print spec details when spec related data is sent in polling response', () => { + let specResult = JSON.stringify({"path": "path"}) + let data = JSON.stringify([specResult]) + var printSpecData = sandbox.stub(); + syncSpecsLogs.__set__('printSpecData', printSpecData); + showSpecsStatus(data); + expect(printSpecData.calledOnce).to.be.true; + }); + + it('should print initial and spec details when spec related data is sent in polling response', () => { + let specResult = JSON.stringify({"path": "path"}) + let data = JSON.stringify(["created", specResult]) + var printSpecData = sandbox.stub(); + syncSpecsLogs.__set__('printSpecData', printSpecData); + var printInitialLog = sandbox.stub(); + syncSpecsLogs.__set__('printInitialLog', printInitialLog); + showSpecsStatus(data); + expect(printSpecData.calledOnce).to.be.true; + expect(printInitialLog.calledOnce).to.be.true; + }); + }); + + context("printSpecsStatus", () => { + const printSpecsStatus = syncSpecsLogs.__get__("printSpecsStatus"); + let startTime = Date.now(), endTime = Date.now() + 10, counter = 0; + let specSummary = { specs: [] }, getOptions, getTableConfig, tableStream, whileProcess; + + beforeEach(() => { + counter = 0; + + getOptions = sandbox.stub(); + syncSpecsLogs.__set__('getOptions', getOptions); + + getTableConfig = sandbox.stub(); + syncSpecsLogs.__set__('getTableConfig', getTableConfig); + + tableStream = sandbox.stub(); + syncSpecsLogs.__set__('tableStream', tableStream); + + whileProcess = sandbox.stub().callsFake(function (whilstCallback) { + counter++ + if(counter >= 3) { + syncSpecsLogs.__set__('whileLoop', false); + whilstCallback(new Error("ggg"), {}); + } else {whileProcess(whilstCallback, 10, null)} + }); + + syncSpecsLogs.__set__('whileProcess', whileProcess); + }); + + it('Should not loop when whileLoop is false and set duration correctly', () => { + syncSpecsLogs.__set__('whileLoop', false); + syncSpecsLogs.__set__('startTime', startTime); + syncSpecsLogs.__set__('endTime', endTime); + syncSpecsLogs.__set__('specSummary', specSummary); + + return printSpecsStatus({}, {}).then((specSummary) => { + expect(getOptions.calledOnce).to.be.true; + expect(getTableConfig.calledOnce).to.be.true; + expect(tableStream.calledOnce).to.be.true; + expect(whileProcess.calledOnce).to.be.false; + expect(specSummary.specs).deep.to.equal([]) + expect(specSummary.duration).to.eql(endTime - startTime); + }); + }); + + it('Should loop when whileLoop is true until it becomes false', () => { + syncSpecsLogs.__set__('whileLoop', true); + syncSpecsLogs.__set__('startTime', startTime); + syncSpecsLogs.__set__('endTime', endTime); + syncSpecsLogs.__set__('specSummary', specSummary); + + return printSpecsStatus({}, {}).then((specSummary) => { + expect(getOptions.calledOnce).to.be.true; + expect(getTableConfig.calledOnce).to.be.true; + expect(tableStream.calledOnce).to.be.true; + expect(whileProcess.callCount).to.eql(3); + expect(specSummary.duration).to.eql(endTime - startTime); + }); + }); + }); + + context("whileProcess", () => { + const whileProcess = syncSpecsLogs.__get__("whileProcess"); + + it('Should retry when request fails with error', () => { + let delayed_n = 2, timeout = 3000, n = 1; + let error = new Error("error"); + + let requestStub = sandbox.stub(); + + let postStub = sandbox + .stub(request, "post") + .yields(error, { statusCode: 502 }, JSON.stringify({})); + + requestStub.post = postStub; + + let setTimeout = sandbox.stub(); + syncSpecsLogs.__set__('setTimeout', setTimeout); + syncSpecsLogs.__set__('n', n); + syncSpecsLogs.__set__('timeout', timeout); + syncSpecsLogs.__set__('request', requestStub); + syncSpecsLogs.__set__('whileTries', 5); + + let whilstCallback = sandbox.stub(); + whileProcess(whilstCallback); + + sinon.assert.calledWith(setTimeout, whilstCallback, timeout * delayed_n, null); + expect(syncSpecsLogs.__get__("whileTries")).to.equal(4); + }); + + it('Should exit after defined number of retries in case of error', () => { + let error = new Error("error"), requestStub = sandbox.stub(); + + let postStub = sandbox + .stub(request, "post") + .yields(error, { statusCode: 502 }, JSON.stringify({})); + + requestStub.post = postStub; + + syncSpecsLogs.__set__('request', requestStub); + syncSpecsLogs.__set__('whileTries', 1); + syncSpecsLogs.__set__('specSummary', {}); + syncSpecsLogs.__set__('whileLoop', true); + + let whilstCallback = sandbox.stub(); + whileProcess(whilstCallback); + + sinon.assert.calledWith(whilstCallback, { status: 504, message: "Tries limit reached" }); + expect(syncSpecsLogs.__get__("whileTries")).to.equal(0); + expect(syncSpecsLogs.__get__("whileLoop")).to.equal(false); + expect(syncSpecsLogs.__get__("specSummary.exitCode")).to.equal(2); + }); + + it('Should print spec details when data is returned from server', () => { + let error = null, body={}, status = 202, n = 1, delayed_n = 2, timeout = 3000; + let requestStub = sandbox.stub(); + let postStub = sandbox + .stub(request, "post") + .yields(error, { statusCode: status }, JSON.stringify(body)); + requestStub.post = postStub; + syncSpecsLogs.__set__('request', requestStub); + + let showSpecsStatus = sandbox.stub(); + syncSpecsLogs.__set__('showSpecsStatus', showSpecsStatus); + + let setTimeout = sandbox.stub(); + syncSpecsLogs.__set__('setTimeout', setTimeout); + syncSpecsLogs.__set__('n', n); + syncSpecsLogs.__set__('timeout', timeout); + + let whilstCallback = sandbox.stub(); + whileProcess(whilstCallback); + + expect(syncSpecsLogs.__get__("n")).to.equal(delayed_n); + sinon.assert.calledWith(setTimeout, whilstCallback, timeout * delayed_n, null); + sinon.assert.calledWith(showSpecsStatus, JSON.stringify(body)); + }); + + it('Should poll for data when server responds with no data available', () => { + let error = null, body={}, status = 204, n = 1, delayed_n = 1, timeout = 3000; + let requestStub = sandbox.stub(); + let postStub = sandbox + .stub(request, "post") + .yields(error, { statusCode: status }, JSON.stringify(body)); + requestStub.post = postStub; + syncSpecsLogs.__set__('request', requestStub); + + let showSpecsStatus = sandbox.stub(); + syncSpecsLogs.__set__('showSpecsStatus', showSpecsStatus); + + let setTimeout = sandbox.stub(); + syncSpecsLogs.__set__('setTimeout', setTimeout); + syncSpecsLogs.__set__('n', n); + syncSpecsLogs.__set__('timeout', timeout); + + let whilstCallback = sandbox.stub(); + whileProcess(whilstCallback); + + expect(syncSpecsLogs.__get__("n")).to.equal(delayed_n); + sinon.assert.calledWith(setTimeout, whilstCallback, timeout * delayed_n, null); + }); + + it('Should stop polling for data when server responds build is completed', () => { + let error = null, body={}, status = 200, n = 1, timeout = 3000; + let requestStub = sandbox.stub(); + let postStub = sandbox.stub(request, "post").yields(error, { statusCode: status }, JSON.stringify(body)); + requestStub.post = postStub; + syncSpecsLogs.__set__('request', requestStub); + + let showSpecsStatus = sandbox.stub(); + syncSpecsLogs.__set__('showSpecsStatus', showSpecsStatus); + + syncSpecsLogs.__set__('whileLoop', true); + syncSpecsLogs.__set__('n', n); + syncSpecsLogs.__set__('timeout', timeout); + + let whilstCallback = sandbox.stub(); + whileProcess(whilstCallback); + + expect(syncSpecsLogs.__get__("whileLoop")).to.be.false; + sinon.assert.calledWith(whilstCallback, null, JSON.stringify(body)); + sinon.assert.calledWith(showSpecsStatus, JSON.stringify(body)); + }); + + it('Should stop polling for data when server responds with error ', () => { + let error = null, body={}, status = 404, n = 1, timeout = 3000; + let requestStub = sandbox.stub(); + let postStub = sandbox.stub(request, "post").yields(error, { statusCode: status }, JSON.stringify(body)); + requestStub.post = postStub; + syncSpecsLogs.__set__('request', requestStub); + + let showSpecsStatus = sandbox.stub(); + syncSpecsLogs.__set__('showSpecsStatus', showSpecsStatus); + + syncSpecsLogs.__set__('whileLoop', true); + syncSpecsLogs.__set__('n', n); + syncSpecsLogs.__set__('timeout', timeout); + + let whilstCallback = sandbox.stub(); + whileProcess(whilstCallback); + + expect(syncSpecsLogs.__get__("whileLoop")).to.be.false; + sinon.assert.calledWith(whilstCallback, {message: JSON.stringify(body), status: status}); + }); + }); +}); diff --git a/test/unit/bin/helpers/utils.js b/test/unit/bin/helpers/utils.js index 1222ffde..56d082d9 100644 --- a/test/unit/bin/helpers/utils.js +++ b/test/unit/bin/helpers/utils.js @@ -5,12 +5,14 @@ const chai = require('chai'), expect = chai.expect, sinon = require('sinon'), chaiAsPromised = require('chai-as-promised'), + chalk = require('chalk'), fs = require('fs'); const utils = require('../../../../bin/helpers/utils'), constant = require('../../../../bin/helpers/constants'), logger = require('../../../../bin/helpers/logger').winstonLogger, - testObjects = require('../../support/fixtures/testObjects'); + testObjects = require('../../support/fixtures/testObjects'), + syncLogger = require("../../../../bin/helpers/logger").syncCliLogger; chai.use(chaiAsPromised); logger.transports['console.info'].silent = true; @@ -181,37 +183,22 @@ describe('utils', () => { }); }); - describe('validateBstackJson', () => { - it('should reject with SyntaxError for empty file', () => { - let bsConfigPath = path.join( - process.cwd(), - 'test', - 'test_files', - 'dummy_bstack.json' - ); - expect(utils.validateBstackJson(bsConfigPath)).to.be.rejectedWith( - SyntaxError - ); + describe("validateBstackJson", () => { + it("should reject with SyntaxError for empty file", () => { + let bsConfigPath = path.join(process.cwd(), 'test', 'test_files', 'dummy_bstack.json'); + return utils.validateBstackJson(bsConfigPath).catch((error)=>{ + sinon.match(error, "Invalid browserstack.json file") + }); }); - it('should resolve with data for valid json', () => { - let bsConfigPath = path.join( - process.cwd(), - 'test', - 'test_files', - 'dummy_bstack_2.json' - ); + it("should resolve with data for valid json", () => { + let bsConfigPath = path.join(process.cwd(), 'test', 'test_files', 'dummy_bstack_2.json'); expect(utils.validateBstackJson(bsConfigPath)).to.be.eventually.eql({}); }); - it('should reject with SyntaxError for invalid json file', () => { - let bsConfigPath = path.join( - process.cwd(), - 'test', - 'test_files', - 'dummy_bstack_3.json' - ); - expect(utils.validateBstackJson(bsConfigPath)).to.be.rejectedWith( - SyntaxError - ); + it("should reject with SyntaxError for invalid json file", () => { + let bsConfigPath = path.join(process.cwd(), 'test', 'test_files', 'dummy_bstack_3.json'); + return utils.validateBstackJson(bsConfigPath).catch((error) => { + sinon.match(error, "Invalid browserstack.json file") + }); }); }); @@ -946,4 +933,54 @@ describe('utils', () => { expect(utils.isUndefined(bsConfig.auth)).to.be.true; }); }); + + describe('capitalizeFirstLetter', () => { + + it('should capitalize First Letter ', () => { + expect(utils.capitalizeFirstLetter("chrome")).to.eq("Chrome"); + }); + + it('should return null if value passed is null', () => { + expect(utils.capitalizeFirstLetter(null)).to.eq(null); + }); + + }); + + describe('#handleSyncExit', () => { + let processStub; + beforeEach(function () { + processStub = sinon.stub(process, 'exit'); + }); + + afterEach(function () { + processStub.restore(); + }); + it('should print network error message when exit code is set to network error code', () => { + let dashboard_url = "dashboard_url", exitCode = 2; + let getNetworkErrorMessageStub = sinon.stub(utils, 'getNetworkErrorMessage'); + utils.handleSyncExit(exitCode, dashboard_url); + sinon.assert.calledOnce(getNetworkErrorMessageStub); + sinon.assert.calledOnceWithExactly(processStub, exitCode); + getNetworkErrorMessageStub.restore(); + }); + + it('should print dashboard link when exit code is not network error code', () => { + let dashboard_url = "dashboard_url", exitCode = 1; + let syncCliLoggerStub = sinon.stub(syncLogger, 'info'); + utils.handleSyncExit(exitCode, dashboard_url); + sinon.assert.calledTwice(syncCliLoggerStub); + sinon.assert.calledOnceWithExactly(processStub, exitCode); + }); + }); + + describe('#getNetworkErrorMessage', () => { + it('should return the error message in red color', () => { + let dashboard_url = "dashboard_url"; + let message = constant.userMessages.FATAL_NETWORK_ERROR + '\n' + + constant.userMessages.RETRY_LIMIT_EXCEEDED + '\n' + + constant.userMessages.CHECK_DASHBOARD_AT + dashboard_url + utils.getNetworkErrorMessage(dashboard_url); + expect(utils.getNetworkErrorMessage(dashboard_url)).to.eq(chalk.red(message)) + }); + }); }); diff --git a/test/unit/bin/helpers/zipUpload.js b/test/unit/bin/helpers/zipUpload.js index c50cf940..a2b3e5b5 100644 --- a/test/unit/bin/helpers/zipUpload.js +++ b/test/unit/bin/helpers/zipUpload.js @@ -22,6 +22,7 @@ describe("zipUpload", () => { sandbox = sinon.createSandbox(); getUserAgentStub = sandbox.stub().returns("random user-agent"); createReadStreamStub = sandbox.stub(fs, "createReadStream"); + deleteZipStub = sandbox.stub().returns(true); }); afterEach(() => { @@ -119,11 +120,14 @@ describe("zipUpload", () => { .stub(request, "post") .yields(null, { statusCode: 200 }, JSON.stringify({ zip_url: zip_url })); - const zipUploader = proxyquire("../../../../bin/helpers/zipUpload", { - "./utils": { + const zipUploader = proxyquire('../../../../bin/helpers/zipUpload', { + './utils': { getUserAgent: getUserAgentStub, }, - request: { post: requestStub }, + request: {post: requestStub}, + './fileHelpers': { + deleteZip: deleteZipStub, + }, }); return zipUploader @@ -132,6 +136,7 @@ describe("zipUpload", () => { sinon.assert.calledOnce(requestStub); sinon.assert.calledOnce(getUserAgentStub); sinon.assert.calledOnce(createReadStreamStub); + sinon.assert.calledOnce(deleteZipStub); chai.assert.equal(data.zip_url, zip_url); }) .catch((error) => { diff --git a/test/unit/support/fixtures/testObjects.js b/test/unit/support/fixtures/testObjects.js index 432fe96e..5aa00ee5 100644 --- a/test/unit/support/fixtures/testObjects.js +++ b/test/unit/support/fixtures/testObjects.js @@ -10,6 +10,19 @@ const sampleBsConfig = { } }; +const sampleBsConfigWithParallels = { + auth: { + username: "random-username", + access_key: "random-access-key", + }, + run_settings: { + cypress_proj_dir: "random path", + cypressConfigFilePath: "random path", + cypressProjectDir: "random path", + parallels: 10, + }, +}; + const initSampleArgs = { _: ["init"], p: false, @@ -114,6 +127,7 @@ const runSampleArgs = { module.exports = Object.freeze({ sampleBsConfig, + sampleBsConfigWithParallels, initSampleArgs, buildInfoSampleArgs, buildInfoSampleBody,