Skip to content

Commit 2a40f31

Browse files
committed
--filter-path= option to do exact (non-regex) prefix filtering
1 parent 7115e86 commit 2a40f31

File tree

7 files changed

+185
-31
lines changed

7 files changed

+185
-31
lines changed

lib/command.js

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,8 @@ function parseOptions(argv, isWindows) {
9292
let color = process.stdout.isTTY || false;
9393
let reporter;
9494
let configPath;
95-
let filter;
95+
let filterRegex;
96+
let filterPath;
9697
let failFast;
9798
let random;
9899
let seed;
@@ -105,7 +106,18 @@ function parseOptions(argv, isWindows) {
105106
} else if (arg === '--color') {
106107
color = true;
107108
} else if (arg.match("^--filter=")) {
108-
filter = arg.match("^--filter=(.*)")[1];
109+
filterRegex = arg.match("^--filter=(.*)")[1];
110+
} else if (arg.match("^--filter-path=(.*)")) {
111+
const json = arg.match("^--filter-path=(.*)")[1];
112+
try {
113+
filterPath = JSON.parse(json);
114+
} catch {
115+
usageErrors.push('Invalid filter path: ' + json);
116+
}
117+
118+
if (!(filterPath instanceof Array)) {
119+
usageErrors.push('Invalid filter path: ' + json);
120+
}
109121
} else if (arg.match("^--helper=")) {
110122
helpers.push(arg.match("^--helper=(.*)")[1]);
111123
} else if (arg.match("^--require=")) {
@@ -142,14 +154,19 @@ function parseOptions(argv, isWindows) {
142154
}
143155
}
144156

157+
if (numWorkers > 1 && filterPath) {
158+
usageErrors.push('--filter-path is not supported in parallel mode.');
159+
}
160+
145161
if (unknownOptions.length > 0) {
146162
usageErrors.push('Unknown options: ' + unknownOptions.join(', '));
147163
}
148164

149165
return {
150166
color,
151167
configPath,
152-
filter,
168+
filterRegex,
169+
filterPath,
153170
failFast,
154171
helpers,
155172
requires,
@@ -209,8 +226,16 @@ async function runJasmine(Jasmine, ParallelRunner, projectBaseDir, options) {
209226

210227
runner.showColors(options.color);
211228

229+
let filter;
230+
231+
if (options.filterRegex) {
232+
filter = options.filterRegex;
233+
} else if (options.filterPath) {
234+
filter = {path: options.filterPath};
235+
}
236+
212237
try {
213-
await runner.execute(options.files, options.filter);
238+
await runner.execute(options.files, filter);
214239
} catch (error) {
215240
console.error(error);
216241
process.exit(1);
@@ -319,6 +344,9 @@ function help(deps) {
319344
{ syntax: '--no-color', help: 'turn off color in spec output' },
320345
{ syntax: '--color', help: 'force turn on color in spec output' },
321346
{ syntax: '--filter=', help: 'filter specs to run only those that match the given regular expression' },
347+
{ syntax: '--filter-path=', help: 'run only the spec or suite that matches ' +
348+
'the given path, e.g. ' +
349+
'--filter-path=\'["parent suite name","child suite name","spec name"]\'' },
322350
{ syntax: '--helper=', help: 'load helper files that match the given string' },
323351
{ syntax: '--require=', help: 'load module that matches the given string' },
324352
{ syntax: '--fail-fast', help: 'stop Jasmine execution on spec failure' },

lib/filters/path_spec_filter.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
module.exports = exports = pathSpecFilter;
2+
3+
function pathSpecFilter(filterPath) {
4+
return function(spec) {
5+
const specPath = spec.getPath();
6+
7+
if (filterPath.length > specPath.length) {
8+
return false;
9+
}
10+
11+
for (let i = 0; i < filterPath.length; i++) {
12+
if (specPath[i] !== filterPath[i]) {
13+
return false;
14+
}
15+
}
16+
17+
return true;
18+
};
19+
}

lib/jasmine.js

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
const ExitHandler = require('./exit_handler');
22
const regexSpecFilter = require('./filters/regex_spec_filter');
3+
const pathSpecFilter = require('./filters/path_spec_filter');
34
const RunnerBase = require('./runner_base');
45

56
/**
@@ -181,11 +182,29 @@ class Jasmine extends RunnerBase {
181182
* ordinarily exit before the promise is settled.
182183
* @param {Array.<string>} [files] Spec files to run instead of the previously
183184
* configured set
184-
* @param {string} [filterString] Regex used to filter specs. If specified, only
185+
* @param {string|object} [filter] Optional specification of what specs to run.
186+
* Can be either a string or an object. If it's a string, it will be
187+
* interpreted as a regex that matches the full names of specs to run. If it's
188+
* an object, it should have a path property whose value is an array of spec
189+
* or suite descriptions.
190+
* Regex used to filter specs. If specified, only
185191
* specs with matching full names will be run.
186192
* @return {Promise<JasmineDoneInfo>} Promise that is resolved when the suite completes.
193+
* @example
194+
* // Run all specs
195+
* await jasmine.execute();
196+
*
197+
* // Run just spec/someSpec.js
198+
* await jasmine.execute(['spec/someSpec.js']);
199+
*
200+
* // Run all specs with full paths starting with "a suite a child suite"
201+
* await jasmine.execute(null, '^a suite a child suite');
202+
*
203+
* // Run all specs that are inside a suite named "a child suite" that is
204+
* // inside a top-level suite named "a suite"
205+
* await jasmine.execute(null, {path: ['a suite', 'a child suite']});
187206
*/
188-
async execute(files, filterString) {
207+
async execute(files, filter) {
189208
await this.loadRequires();
190209
await this.loadHelpers();
191210
if (!this.defaultReporterConfigured) {
@@ -195,10 +214,18 @@ class Jasmine extends RunnerBase {
195214
});
196215
}
197216

198-
if (filterString) {
199-
this.env.configure({
200-
specFilter: regexSpecFilter(filterString)
201-
});
217+
if (filter) {
218+
if (typeof filter === 'string') {
219+
this.env.configure({
220+
specFilter: regexSpecFilter(filter)
221+
});
222+
} else if (filter.path) {
223+
this.env.configure({
224+
specFilter: pathSpecFilter(filter.path)
225+
});
226+
} else {
227+
throw new Error('Unrecognized filter type');
228+
}
202229
}
203230

204231
if (files && files.length > 0) {

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
],
3333
"dependencies": {
3434
"glob": "^10.2.2",
35-
"jasmine-core": "~5.5.0"
35+
"jasmine-core": "github:jasmine/jasmine"
3636
},
3737
"bin": "./bin/jasmine.js",
3838
"main": "./lib/jasmine.js",

spec/command_spec.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -507,6 +507,26 @@ describe('command', function() {
507507
stopSpecOnExpectationFailure: true
508508
});
509509
});
510+
511+
describe('Path filtering', function() {
512+
it('should be able to filter by path', async function () {
513+
await this.run(['--filter-path=["a","b","c"]']);
514+
expect(this.runner.execute).toHaveBeenCalledWith(jasmine.any(Array),
515+
{path: ['a', 'b', 'c']});
516+
});
517+
518+
it('rejects invalid JSON', async function() {
519+
await this.run(['--filter-path=not valid JSON']);
520+
const output = this.out.getOutput();
521+
expect(output).toContain('Invalid filter path: not valid JSON');
522+
});
523+
524+
it('rejects non-arrays', async function() {
525+
await this.run(['--filter-path={}}']);
526+
const output = this.out.getOutput();
527+
expect(output).toContain('Invalid filter path: {}}');
528+
});
529+
});
510530
});
511531

512532
describe('In parallel mode', function() {
@@ -515,6 +535,13 @@ describe('command', function() {
515535
});
516536

517537
sharedRunBehavior('--parallel=2');
538+
539+
it('rejects --filter-path', async function() {
540+
await this.run(['--filter-path=[]', '--parallel=2']);
541+
const output = this.out.getOutput();
542+
expect(output).toContain(
543+
'--filter-path is not supported in parallel mode.');
544+
});
518545
});
519546
});
520547

spec/jasmine_spec.js

Lines changed: 47 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -229,32 +229,61 @@ describe('Jasmine', function() {
229229
expect(this.testJasmine.configureDefaultReporter).toHaveBeenCalled();
230230
});
231231

232-
describe('When a filter string is provided', function() {
233-
it('installs a matching spec filter', async function() {
234-
let specFilter;
235-
this.testJasmine.env.configure.and.callFake(function(config) {
232+
describe('Filtering', function() {
233+
let specFilter;
234+
235+
beforeEach(function() {
236+
this.testJasmine.env.configure.and.callFake(function (config) {
236237
if (config.specFilter) {
237238
specFilter = config.specFilter;
238239
}
239240
});
241+
});
240242

241-
await this.execute({
242-
executeArgs: [['spec/fixtures/example/*spec.js'], 'interesting spec']
243+
describe('When a filter string is provided', function () {
244+
it('installs a matching spec filter', async function () {
245+
await this.execute({
246+
executeArgs: [['spec/fixtures/example/*spec.js'], 'interesting spec']
247+
});
248+
249+
expect(specFilter).toBeTruthy();
250+
const matchingSpec = {
251+
getFullName() {
252+
return 'this is an interesting spec that should match';
253+
}
254+
};
255+
const nonMatchingSpec = {
256+
getFullName() {
257+
return 'but this one is not';
258+
}
259+
};
260+
expect(specFilter(matchingSpec)).toBeTrue();
261+
expect(specFilter(nonMatchingSpec)).toBeFalse();
243262
});
263+
});
244264

245-
expect(specFilter).toBeTruthy();
246-
const matchingSpec = {
247-
getFullName() {
248-
return 'this is an interesting spec that should match';
249-
}
250-
};
251-
const nonMatchingSpec = {
252-
getFullName() {
253-
return 'but this one is not';
265+
describe('When a path filter specification is provided', function () {
266+
it('installs a matching spec filter', async function () {
267+
await this.execute({
268+
executeArgs: [['spec/fixtures/example/*spec.js'], {
269+
path: ['parent', 'child', 'spec']
270+
}]
271+
});
272+
273+
function stubSpec(path) {
274+
return {
275+
getPath() { return path; },
276+
// getFullName is required, but plays no role in filtering
277+
getFullName() { return ""; }
278+
};
254279
}
255-
};
256-
expect(specFilter(matchingSpec)).toBeTrue();
257-
expect(specFilter(nonMatchingSpec)).toBeFalse();
280+
281+
expect(specFilter).toBeTruthy();
282+
expect(specFilter(stubSpec(['parent', 'child', 'spec'])))
283+
.toBeTrue();
284+
expect(specFilter(stubSpec(['parent', 'other child', 'spec'])))
285+
.toBeFalse();
286+
});
258287
});
259288
});
260289

@@ -443,8 +472,6 @@ describe('Jasmine', function() {
443472
});
444473

445474
describe('#enumerate', function() {
446-
it('does not run global setup');
447-
448475
it('loads requires, helpers, and specs', async function() {
449476
const loadRequires = spyOn(this.testJasmine, 'loadRequires');
450477
const loadHelpers = spyOn(this.testJasmine, 'loadHelpers');

spec/path_spec_filter_spec.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
const pathSpecFilter = require('../lib/filters/path_spec_filter');
2+
3+
describe("pathSpecFilter", function() {
4+
it("matches a spec with the exact same path", function() {
5+
const specFilter = pathSpecFilter(["a", "b", "c"]);
6+
expect(specFilter(stubSpec(['a', 'b', 'c']))).toBeTrue();
7+
});
8+
9+
it('matches a spec whose path has the filter path as a prefix', function() {
10+
const specFilter = pathSpecFilter(["a", "b"]);
11+
expect(specFilter(stubSpec(['a', 'b', 'c']))).toBeTrue();
12+
});
13+
14+
it('does not match a spec with a different path', function() {
15+
const specFilter = pathSpecFilter(["a", "b", "c"]);
16+
expect(specFilter(stubSpec(['a', 'd', 'c']))).toBeFalse();
17+
});
18+
19+
function stubSpec(path) {
20+
return {
21+
getPath() { return path; },
22+
// getFullName is required, but plays no role in filtering
23+
getFullName() { return ""; }
24+
};
25+
}
26+
});

0 commit comments

Comments
 (0)