diff --git a/bin/commands/runs.js b/bin/commands/runs.js index 4eea327e..9d69c3cb 100644 --- a/bin/commands/runs.js +++ b/bin/commands/runs.js @@ -45,10 +45,13 @@ module.exports = function run(args) { utils.setLocalIdentifier(bsConfig); // Validate browserstack.json values and parallels specified via arguments - return capabilityHelper.validate(bsConfig, args).then(function (validated) { + return capabilityHelper.validate(bsConfig, args).then(function (cypressJson) { + + //get the number of spec files + let specFiles = utils.getNumberOfSpecFiles(bsConfig, args, cypressJson); // accept the number of parallels - utils.setParallels(bsConfig, args); + utils.setParallels(bsConfig, args, specFiles.length); // Archive the spec files return archiver.archive(bsConfig.run_settings, config.fileName, args.exclude).then(function (data) { diff --git a/bin/helpers/capabilityHelper.js b/bin/helpers/capabilityHelper.js index c99851da..442b88ba 100644 --- a/bin/helpers/capabilityHelper.js +++ b/bin/helpers/capabilityHelper.js @@ -126,6 +126,7 @@ const validate = (bsConfig, args) => { // validate if config file provided exists or not when cypress_config_file provided // validate the cypressProjectDir key otherwise. let cypressConfigFilePath = bsConfig.run_settings.cypressConfigFilePath; + let cypressJson = {}; if (!fs.existsSync(cypressConfigFilePath) && bsConfig.run_settings.cypress_config_filename !== 'false') reject(Constants.validationMessages.INVALID_CYPRESS_CONFIG_FILE); @@ -143,8 +144,7 @@ const validate = (bsConfig, args) => { } catch(error){ reject(Constants.validationMessages.INVALID_CYPRESS_JSON) } - - resolve(Constants.validationMessages.VALIDATED); + resolve(cypressJson); }); } diff --git a/bin/helpers/constants.js b/bin/helpers/constants.js index db0852f8..e0a085e6 100644 --- a/bin/helpers/constants.js +++ b/bin/helpers/constants.js @@ -113,6 +113,10 @@ const allowedFileTypes = ['js', 'json', 'txt', 'ts', 'feature', 'features', 'pdf const filesToIgnoreWhileUploading = ['**/node_modules/**', 'node_modules/**', 'package-lock.json', 'package.json', 'browserstack-package.json', 'tests.zip', 'cypress.json'] +const specFileTypes = ['js', 'ts', 'feature'] + +const DEFAULT_CYPRESS_SPEC_PATH = "cypress/integration" + module.exports = Object.freeze({ syncCLI, userMessages, @@ -120,5 +124,7 @@ module.exports = Object.freeze({ validationMessages, messageTypes, allowedFileTypes, - filesToIgnoreWhileUploading + filesToIgnoreWhileUploading, + specFileTypes, + DEFAULT_CYPRESS_SPEC_PATH }); diff --git a/bin/helpers/utils.js b/bin/helpers/utils.js index 116b0355..5d834a22 100644 --- a/bin/helpers/utils.js +++ b/bin/helpers/utils.js @@ -2,6 +2,7 @@ const os = require("os"); const path = require("path"); const fs = require("fs"); +const glob = require('glob'); const usageReporting = require("./usageReporting"), logger = require("./logger").winstonLogger, @@ -112,10 +113,22 @@ exports.setUsageReportingFlag = (bsConfig, disableUsageReporting) => { } }; -exports.setParallels = (bsConfig, args) => { +exports.setParallels = (bsConfig, args, numOfSpecs) => { if (!this.isUndefined(args.parallels)) { bsConfig["run_settings"]["parallels"] = args.parallels; } + let browserCombinations = this.getBrowserCombinations(bsConfig); + let maxParallels = browserCombinations.length * numOfSpecs; + if (numOfSpecs <= 0) { + bsConfig['run_settings']['parallels'] = browserCombinations.length; + return; + } + if (bsConfig['run_settings']['parallels'] > maxParallels && bsConfig['run_settings']['parallels'] != -1 ) { + logger.warn( + `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; + } }; exports.setDefaults = (bsConfig, args) => { @@ -320,6 +333,27 @@ exports.setLocalIdentifier = (bsConfig) => { } }; +exports.getNumberOfSpecFiles = (bsConfig, args, cypressJson) => { + let testFolderPath = cypressJson.integrationFolder || Constants.DEFAULT_CYPRESS_SPEC_PATH; + let globSearchPatttern = bsConfig.run_settings.specs || `${testFolderPath}/**/*.+(${Constants.specFileTypes.join("|")})`; + let ignoreFiles = args.exclude || bsConfig.run_settings.exclude; + let files = glob.sync(globSearchPatttern, {cwd: bsConfig.run_settings.cypressProjectDir, matchBase: true, ignore: ignoreFiles}); + return files; +}; + +exports.getBrowserCombinations = (bsConfig) => { + let osBrowserArray = []; + let osBrowser = ""; + if (bsConfig.browsers) { + bsConfig.browsers.forEach((element) => { + osBrowser = element.os + '-' + element.browser; + element.versions.forEach((version) => { + osBrowserArray.push(osBrowser + version); + }); + }); + } + return osBrowserArray; +}; exports.capitalizeFirstLetter = (stringToCapitalize) => { return stringToCapitalize && (stringToCapitalize[0].toUpperCase() + stringToCapitalize.slice(1)); }; diff --git a/package.json b/package.json index 111f2eed..ae80a642 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,8 @@ "requestretry": "^4.1.0", "table": "^5.4.6", "winston": "^2.3.1", - "yargs": "^14.2.2" + "yargs": "^14.2.2", + "glob": "^7.1.6" }, "repository": { "type": "git", diff --git a/test/unit/bin/commands/runs.js b/test/unit/bin/commands/runs.js index a57e0b30..29e02007 100644 --- a/test/unit/bin/commands/runs.js +++ b/test/unit/bin/commands/runs.js @@ -188,6 +188,7 @@ describe("runs", () => { setLocalStub = sandbox.stub(); setLocalIdentifierStub = sandbox.stub(); deleteResultsStub = sandbox.stub(); + getNumberOfSpecFilesStub = sandbox.stub().returns([]); setDefaultsStub = sandbox.stub(); }); @@ -217,7 +218,8 @@ describe("runs", () => { setLocal: setLocalStub, setLocalIdentifier: setLocalIdentifierStub, deleteResults: deleteResultsStub, - setDefaults: setDefaultsStub + setDefaults: setDefaultsStub, + getNumberOfSpecFiles: getNumberOfSpecFilesStub }, '../helpers/capabilityHelper': { validate: capabilityValidatorStub, @@ -241,6 +243,7 @@ describe("runs", () => { .catch((error) => { sinon.assert.calledOnce(getConfigPathStub); sinon.assert.calledOnce(getConfigPathStub); + sinon.assert.calledOnce(getNumberOfSpecFilesStub); sinon.assert.calledOnce(setParallelsStub); sinon.assert.calledOnce(setLocalStub); sinon.assert.calledOnce(setLocalIdentifierStub); @@ -288,6 +291,7 @@ describe("runs", () => { setLocalStub = sandbox.stub(); setLocalIdentifierStub = sandbox.stub(); deleteResultsStub = sandbox.stub(); + getNumberOfSpecFilesStub = sandbox.stub().returns([]); setDefaultsStub = sandbox.stub(); }); @@ -317,6 +321,7 @@ describe("runs", () => { setLocal: setLocalStub, setLocalIdentifier: setLocalIdentifierStub, deleteResults: deleteResultsStub, + getNumberOfSpecFiles: getNumberOfSpecFilesStub, setDefaults: setDefaultsStub }, '../helpers/capabilityHelper': { @@ -345,6 +350,7 @@ describe("runs", () => { .catch((error) => { sinon.assert.calledOnce(getConfigPathStub); sinon.assert.calledOnce(getConfigPathStub); + sinon.assert.calledOnce(getNumberOfSpecFilesStub); sinon.assert.calledOnce(setParallelsStub); sinon.assert.calledOnce(setLocalStub); sinon.assert.calledOnce(setLocalIdentifierStub); @@ -396,6 +402,7 @@ describe("runs", () => { setLocalStub = sandbox.stub(); setLocalIdentifierStub = sandbox.stub(); deleteResultsStub = sandbox.stub(); + getNumberOfSpecFilesStub = sandbox.stub().returns([]); setDefaultsStub = sandbox.stub(); }); @@ -425,6 +432,7 @@ describe("runs", () => { setLocal: setLocalStub, setLocalIdentifier: setLocalIdentifierStub, deleteResults: deleteResultsStub, + getNumberOfSpecFiles: getNumberOfSpecFilesStub, setDefaults: setDefaultsStub }, '../helpers/capabilityHelper': { @@ -461,6 +469,7 @@ describe("runs", () => { sinon.assert.calledOnce(getConfigPathStub); sinon.assert.calledOnce(validateBstackJsonStub); sinon.assert.calledOnce(capabilityValidatorStub); + sinon.assert.calledOnce(getNumberOfSpecFilesStub); sinon.assert.calledOnce(setParallelsStub); sinon.assert.calledOnce(setLocalStub); sinon.assert.calledOnce(setLocalIdentifierStub); @@ -516,6 +525,7 @@ describe("runs", () => { isUndefinedStub = sandbox.stub(); setLocalStub = sandbox.stub(); setLocalIdentifierStub = sandbox.stub(); + getNumberOfSpecFilesStub = sandbox.stub().returns([]); }); afterEach(() => { @@ -529,8 +539,8 @@ describe("runs", () => { let message = `Success! ${Constants.userMessages.BUILD_CREATED} with build id: random_build_id`; let dashboardLink = `${Constants.userMessages.VISIT_DASHBOARD} ${dashboardUrl}`; - const runs = proxyquire("../../../../bin/commands/runs", { - "../helpers/utils": { + const runs = proxyquire('../../../../bin/commands/runs', { + '../helpers/utils': { validateBstackJson: validateBstackJsonStub, sendUsageReport: sendUsageReportStub, setUsername: setUsernameStub, @@ -547,24 +557,25 @@ describe("runs", () => { exportResults: exportResultsStub, deleteResults: deleteResultsStub, setDefaults: setDefaultsStub, - isUndefined: isUndefinedStub + isUndefined: isUndefinedStub, + getNumberOfSpecFiles: getNumberOfSpecFilesStub }, - "../helpers/capabilityHelper": { + '../helpers/capabilityHelper': { validate: capabilityValidatorStub, }, - "../helpers/archiver": { + '../helpers/archiver': { archive: archiverStub, }, - "../helpers/fileHelpers": { + '../helpers/fileHelpers': { deleteZip: deleteZipStub, }, - "../helpers/zipUpload": { + '../helpers/zipUpload': { zipUpload: zipUploadStub, }, - "../helpers/build": { + '../helpers/build': { createBuild: createBuildStub, }, - "../helpers/config": { + '../helpers/config': { dashboardUrl: dashboardUrl, }, }); @@ -586,6 +597,7 @@ describe("runs", () => { sinon.assert.calledOnce(getConfigPathStub); sinon.assert.calledOnce(validateBstackJsonStub); sinon.assert.calledOnce(capabilityValidatorStub); + sinon.assert.calledOnce(getNumberOfSpecFilesStub); sinon.assert.calledOnce(setParallelsStub); sinon.assert.calledOnce(setLocalStub); sinon.assert.calledOnce(setLocalIdentifierStub); diff --git a/test/unit/bin/helpers/capabilityHelper.js b/test/unit/bin/helpers/capabilityHelper.js index 0d68d25e..9e379f64 100644 --- a/test/unit/bin/helpers/capabilityHelper.js +++ b/test/unit/bin/helpers/capabilityHelper.js @@ -561,10 +561,7 @@ describe("capabilityHelper.js", () => { return capabilityHelper .validate(bsConfig, { parallels: undefined }) .then(function (data) { - chai.assert.equal(data, Constants.validationMessages.VALIDATED); - }) - .catch((error) => { - chai.assert.fail("Promise error"); + chai.assert.deepEqual(data, {}); }); }); @@ -574,10 +571,7 @@ describe("capabilityHelper.js", () => { return capabilityHelper .validate(bsConfig, { parallels: undefined }) .then(function (data) { - chai.assert.equal(data, Constants.validationMessages.VALIDATED); - }) - .catch((error) => { - chai.assert.fail("Promise error"); + chai.assert.deepEqual(data, {}); }); }); @@ -586,10 +580,7 @@ describe("capabilityHelper.js", () => { return capabilityHelper .validate(bsConfig, { parallels: 200 }) .then(function (data) { - chai.assert.equal(data, Constants.validationMessages.VALIDATED); - }) - .catch((error) => { - chai.assert.fail("Promise error"); + chai.assert.deepEqual(data, {}); }); }); @@ -598,10 +589,7 @@ describe("capabilityHelper.js", () => { return capabilityHelper .validate(bsConfig, { parallels: -1 }) .then(function (data) { - chai.assert.equal(data, Constants.validationMessages.VALIDATED); - }) - .catch((error) => { - chai.assert.fail("Promise error"); + chai.assert.deepEqual(data, {}); }); }); @@ -611,10 +599,7 @@ describe("capabilityHelper.js", () => { return capabilityHelper .validate(bsConfig, { parallels: -1 }) .then(function (data) { - chai.assert.equal(data, Constants.validationMessages.VALIDATED); - }) - .catch((error) => { - chai.assert.fail("Promise error"); + chai.assert.deepEqual(data, {}); }); }); @@ -624,10 +609,7 @@ describe("capabilityHelper.js", () => { return capabilityHelper .validate(bsConfig, { parallels: -1 }) .then(function (data) { - chai.assert.equal(data, Constants.validationMessages.VALIDATED); - }) - .catch((error) => { - chai.assert.fail("Promise error"); + chai.assert.deepEqual(data, {}); }); }); }); diff --git a/test/unit/bin/helpers/utils.js b/test/unit/bin/helpers/utils.js index 143773c8..2b262a33 100644 --- a/test/unit/bin/helpers/utils.js +++ b/test/unit/bin/helpers/utils.js @@ -5,6 +5,7 @@ const chai = require('chai'), expect = chai.expect, sinon = require('sinon'), chaiAsPromised = require('chai-as-promised'), + glob = require('glob'), chalk = require('chalk'), fs = require('fs'); @@ -139,13 +140,25 @@ describe('utils', () => { }); describe('setParallels', () => { + var sandbox; + beforeEach(() => { + sandbox = sinon.createSandbox(); + sandbox.stub(utils,'getBrowserCombinations').returns(['a','b']); + }); + + afterEach(() => { + sandbox.restore(); + sinon.restore(); + }); + it('should set bsconfig parallels equal to value provided in args', () => { let bsConfig = { run_settings: { parallels: 10, }, }; - utils.setParallels(bsConfig, {parallels: 100}); + + utils.setParallels(bsConfig, {parallels: 100}, 100); expect(bsConfig['run_settings']['parallels']).to.be.eq(100); }); @@ -155,9 +168,40 @@ describe('utils', () => { parallels: 10, }, }; - utils.setParallels(bsConfig, {parallels: undefined}); + utils.setParallels(bsConfig, {parallels: undefined}, 10); expect(bsConfig['run_settings']['parallels']).to.be.eq(10); }); + + it('should set bsconfig parallels to browserCombinations.length if numOfSpecs is zero', () => { + let bsConfig = { + run_settings: { + parallels: 10, + }, + }; + utils.setParallels(bsConfig, {parallels: undefined}, 0); + expect(bsConfig['run_settings']['parallels']).to.be.eq(2); + }); + + it('shouldnot set bsconfig parallels if parallels is -1', () => { + let bsConfig = { + run_settings: { + parallels: -1, + }, + }; + utils.setParallels(bsConfig, {parallels: undefined}, 2); + expect(bsConfig['run_settings']['parallels']).to.be.eq(-1); + }); + + it('should set bsconfig parallels if parallels is greater than numOfSpecs * combinations', () => { + let bsConfig = { + run_settings: { + parallels: 100, + }, + }; + utils.setParallels(bsConfig, {parallels: undefined}, 2); + expect(bsConfig['run_settings']['parallels']).to.be.eq(4); + }); + }); describe('getErrorCodeFromErr', () => { @@ -286,7 +330,7 @@ describe('utils', () => { let args = testObjects.initSampleArgs; it('should call sendUsageReport', () => { - sandbox = sinon.createSandbox(); + let sandbox = sinon.createSandbox(); sendUsageReportStub = sandbox .stub(utils, 'sendUsageReport') .callsFake(function () { @@ -942,6 +986,71 @@ describe('utils', () => { }); }); + describe('getNumberOfSpecFiles', () => { + + it('glob search pattern should be equal to bsConfig.run_settings.specs', () => { + let getNumberOfSpecFilesStub = sinon.stub(glob, 'sync'); + let bsConfig = { + run_settings: { + specs: 'specs', + cypressProjectDir: 'cypressProjectDir', + exclude: 'exclude', + }, + }; + + utils.getNumberOfSpecFiles(bsConfig,{},{}); + sinon.assert.calledOnce(getNumberOfSpecFilesStub); + sinon.assert.calledOnceWithExactly(getNumberOfSpecFilesStub, 'specs', { + cwd: 'cypressProjectDir', + matchBase: true, + ignore: 'exclude', + }); + glob.sync.restore(); + }); + + it('glob search pattern should be equal to default', () => { + let getNumberOfSpecFilesStub = sinon.stub(glob, 'sync'); + let bsConfig = { + run_settings: { + cypressProjectDir: 'cypressProjectDir', + exclude: 'exclude', + }, + }; + + utils.getNumberOfSpecFiles(bsConfig,{},{}); + + sinon.assert.calledOnceWithExactly(getNumberOfSpecFilesStub, `cypress/integration/**/*.+(${constant.specFileTypes.join("|")})`, { + cwd: 'cypressProjectDir', + matchBase: true, + ignore: 'exclude', + }); + glob.sync.restore(); + }); + + it('glob search pattern should be equal to default with integrationFolder', () => { + let getNumberOfSpecFilesStub = sinon.stub(glob, 'sync'); + let bsConfig = { + run_settings: { + cypressProjectDir: 'cypressProjectDir', + exclude: 'exclude', + }, + }; + + utils.getNumberOfSpecFiles(bsConfig, {}, { "integrationFolder": "specs"}); + + sinon.assert.calledOnceWithExactly( + getNumberOfSpecFilesStub, + `specs/**/*.+(${constant.specFileTypes.join('|')})`, + { + cwd: 'cypressProjectDir', + matchBase: true, + ignore: 'exclude', + } + ); + glob.sync.restore(); + }); + }); + describe('capitalizeFirstLetter', () => { it('should capitalize First Letter ', () => { @@ -954,6 +1063,45 @@ describe('utils', () => { }); + describe('getBrowserCombinations', () => { + + it('returns correct number of browserCombinations for one combination', () => { + let bsConfig = { + browsers: [ + { + browser: 'chrome', + os: 'OS X Mojave', + versions: ['85'], + }, + ] + }; + chai.assert.deepEqual(utils.getBrowserCombinations(bsConfig), ['OS X Mojave-chrome85']); + }); + + it('returns correct number of browserCombinations for multiple combinations', () => { + let bsConfig = { + browsers: [ + { + browser: 'chrome', + os: 'OS X Mojave', + versions: ['85'], + }, + { + browser: 'chrome', + os: 'OS X Catalina', + versions: ['85','84'], + }, + ], + }; + chai.assert.deepEqual(utils.getBrowserCombinations(bsConfig), [ + 'OS X Mojave-chrome85', + 'OS X Catalina-chrome85', + 'OS X Catalina-chrome84' + ]); + }); + + }); + describe('#handleSyncExit', () => { let processStub; beforeEach(function () {