diff --git a/packages/server/lib/util/args.js b/packages/server/lib/util/args.js index ea76dc262f37..088f4eac790a 100644 --- a/packages/server/lib/util/args.js +++ b/packages/server/lib/util/args.js @@ -204,6 +204,118 @@ const sanitizeAndConvertNestedArgs = (str, argname) => { } } +/** + * Parses the '--spec' cli parameter to return an array of valid patterns. + * + * @param {Strng} pattern pattern to parse + * @returns Array of patterns + */ +const parseSpecArgv = (pattern) => { + const TOKENS = { + OPEN: ['{', '['], + CLOSE: ['}', ']'], + } + const hasToken = [...TOKENS.OPEN, ...TOKENS.CLOSE].some((t) => { + return pattern.includes(t) + }) + const hasComma = pattern.includes(',') + + /** + * Slice and mutate a string. + * + * @param {String} str String to slice & mutate + * @param {Number} end Index to slice to + * @returns [String, String, Number] + */ + const sliceAndMutate = (str, end) => { + return [ + str.slice(0, end), + str.substring(end, str.length), + str.slice(0, end).length, + ] + } + + /** + * Sanitizes a path's leftover commas. + * + * @param {String} path + * @returns String + */ + const sanitizeFinalPath = (path) => { + return path.split('')[0] === ',' ? path.substring(1, path.length) : path + } + + if (!hasToken) { + return [].concat(pattern.split(',')) + } + + if (!hasComma) { + return pattern + } + + // Get comma rules. + let opens = [] + let closes = [] + const rules = pattern + .split('') + .map((token, index) => { + if (TOKENS.OPEN.includes(token)) { + opens.push(index) + } + + if (TOKENS.CLOSE.includes(token)) { + closes.push(index) + } + + if (token === ',') { + const isBreakable = + index > opens[opens.length - 1] && + index > closes[closes.length - 1] && + opens.length === closes.length + + if (isBreakable) { + return { + comma: index, + isBreakable: true, + } + } + + return { + comma: index, + isBreakable: false, + } + } + + return null + }) + .filter(Boolean) + + // Perform comma breaking logic. + let carry = pattern + let offset = 0 + const partial = rules + .map((rule) => { + if (!rule.isBreakable) { + return null + } + + const [res, mutated, offsettedBy] = sliceAndMutate( + carry, + rule.comma - offset, + ) + + offset = offsettedBy + carry = mutated + + return res + }) + .filter(Boolean) + .map(sanitizeFinalPath) + + // In the end, carry will be left with the last path that hasn't been cut. + return [...partial, sanitizeFinalPath(carry)] +} + module.exports = { normalizeBackslashes, @@ -303,12 +415,12 @@ module.exports = { spec = spec.substring(1, spec.length - 1) } - options.spec = strToArray(spec).map(resolvePath) + options.spec = parseSpecArgv(spec).map(resolvePath) } else { options.spec = spec.map(resolvePath) } } catch (err) { - debug('could not pass config spec value %s', spec) + debug('could not parse config spec value %s', spec) debug('error %o', err) return errors.throw('COULD_NOT_PARSE_ARGUMENTS', 'spec', spec, 'spec must be a string or comma-separated list') diff --git a/packages/server/test/unit/util/args_spec.js b/packages/server/test/unit/util/args_spec.js index 3286282a1df2..45c0029b27e1 100644 --- a/packages/server/test/unit/util/args_spec.js +++ b/packages/server/test/unit/util/args_spec.js @@ -164,6 +164,19 @@ describe('lib/util/args', () => { return snapshot('invalid spec error', stripAnsi(err.message)) } }) + + it('should be correctly parsing globs with lists & ranges', function () { + const options = this.setup('--spec', 'cypress/integration/{[!a]*.spec.js,sub1,{sub2,sub3/sub4}}/*.js') + + expect(options.spec[0]).to.eq(`${cwd}/cypress/integration/{[!a]*.spec.js,sub1,{sub2,sub3/sub4}}/*.js`) + }) + + it('should be correctly parsing globs with a mix of lists, ranges & regular paths', function () { + const options = this.setup('--spec', 'cypress/integration/{[!a]*.spec.js,sub1,{sub2,sub3/sub4}}/*.js,cypress/integration/foo.spec.js') + + expect(options.spec[0]).to.eq(`${cwd}/cypress/integration/{[!a]*.spec.js,sub1,{sub2,sub3/sub4}}/*.js`) + expect(options.spec[1]).to.eq(`${cwd}/cypress/integration/foo.spec.js`) + }) }) context('--tag', () => { diff --git a/yarn.lock b/yarn.lock index 01392de97343..4da09eb5a0da 100644 --- a/yarn.lock +++ b/yarn.lock @@ -23008,9 +23008,9 @@ is-glob@^3.0.0, is-glob@^3.1.0: is-extglob "^2.1.0" is-glob@^4.0.0, is-glob@^4.0.1, is-glob@~4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" - integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== dependencies: is-extglob "^2.1.1"