diff --git a/jest.config.js b/jest.config.js index ea5f2b233..d9a567a2d 100644 --- a/jest.config.js +++ b/jest.config.js @@ -13,5 +13,6 @@ module.exports = { preset: 'ts-jest', testEnvironment: 'node', testRegex: '.*\\.spec\\.ts$', - watchPathIgnorePatterns: ['/fixtures', '/packages/[^/]+/src'] + watchPathIgnorePatterns: ['/fixtures', '/packages/[^/]+/src'], + modulePathIgnorePatterns: ['/packages/extension'] }; diff --git a/packages/betterer/src/suite/suite.ts b/packages/betterer/src/suite/suite.ts index 937a6b988..bdf838782 100644 --- a/packages/betterer/src/suite/suite.ts +++ b/packages/betterer/src/suite/suite.ts @@ -10,6 +10,8 @@ import { Defer, defer } from '../utils'; import { BettererSuiteSummaryΩ } from './suite-summary'; import { BettererSuite } from './types'; +const NEGATIVE_FILTER_TOKEN = '!'; + export class BettererSuiteΩ implements BettererSuite { private _reporter: BettererReporterΩ; @@ -60,9 +62,37 @@ export class BettererSuiteΩ implements BettererSuite { runsΩ.map(async (runΩ, index) => { const lifecycle = runLifecycles[index]; - const isFiltered = filters.length && !filters.some((filter) => filter.test(runΩ.name)); + // This is all a bit backwards because "filters" is named badly. + const hasFilters = !!filters.length; + + // And this is some madness which applies filters and negative filters in + // the order they are read: + // + // ["foo"] => [/foo/] => ["foo"] + // ["foo"] => [/bar/] => [] + // ["foo"] => [/!foo/] => [] + // ["foo"] => [/!bar/] => ["foo"] + // ["foo"] => [/foo/, /!foo/] => [] + // ["foo"] => [/!foo/, /foo/] => ["foo"] + const isSelected = filters.reduce((selected, filter) => { + const isNegated = filter.source.startsWith(NEGATIVE_FILTER_TOKEN); + if (selected) { + if (isNegated) { + const negativeFilter = new RegExp(filter.source.substr(1), filter.flags); + return !negativeFilter.test(runΩ.name); + } + return selected; + } else { + if (isNegated) { + const negativeFilter = new RegExp(filter.source.substr(1), filter.flags); + return !negativeFilter.test(runΩ.name); + } + return filter.test(runΩ.name); + } + }, false); + const isOtherTestOnly = hasOnly && !runΩ.testMeta.isOnly; - const isSkipped = isFiltered || isOtherTestOnly || runΩ.testMeta.isSkipped; + const isSkipped = (hasFilters && !isSelected) || isOtherTestOnly || runΩ.testMeta.isSkipped; // Don't await here! A custom reporter could be awaiting // the lifecycle promise which is unresolved right now! diff --git a/packages/cli/src/options.ts b/packages/cli/src/options.ts index 0d7191f1d..48688734a 100644 --- a/packages/cli/src/options.ts +++ b/packages/cli/src/options.ts @@ -82,7 +82,11 @@ function tsconfigPathOption(): void { } function filtersOption(): void { - commander.option('-f, --filter [value]', 'RegExp filter for tests to run. Takes multiple values', argsToArray); + commander.option( + '-f, --filter [value]', + 'RegExp filter for tests to run. Add "!" at the start to negate. Takes multiple values', + argsToArray + ); } function excludesOption(): void { diff --git a/test/cli/__snapshots__/filter-negative.spec.ts.snap b/test/cli/__snapshots__/filter-negative.spec.ts.snap new file mode 100644 index 000000000..edd6c1325 --- /dev/null +++ b/test/cli/__snapshots__/filter-negative.spec.ts.snap @@ -0,0 +1,137 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`betterer cli should filter tests by name with negation 1`] = ` +Array [ + " + / | / _ _ _ + '-.ooo.-' | |__ ___| |_| |_ ___ _ __ ___ _ __ +---ooooo--- | '_ // _ / __| __/ _ / '__/ _ / '__| + .-'ooo'-. | |_)| __/ |_| || __/ | | __/ | + / | / |_.__//___|/__|/__/___|_| /___|_| +", + " + / | / _ _ _ + '-.ooo.-' | |__ ___| |_| |_ ___ _ __ ___ _ __ +---ooooo--- | '_ // _ / __| __/ _ / '__/ _ / '__| + .-'ooo'-. | |_)| __/ |_| || __/ | | __/ | + / | / |_.__//___|/__|/__/___|_| /___|_| + +🌟 Betterer (0ms): +", + " + / | / _ _ _ + '-.ooo.-' | |__ ___| |_| |_ ___ _ __ ___ _ __ +---ooooo--- | '_ // _ / __| __/ _ / '__/ _ / '__| + .-'ooo'-. | |_)| __/ |_| || __/ | | __/ | + / | / |_.__//___|/__|/__/___|_| /___|_| + +🎉 Betterer (0ms): 3 tests done! +✅ test 1: \\"test 1\\" got checked for the first time! 🎉 +✅ test 2: \\"test 2\\" got checked for the first time! 🎉 +✅ test 3: \\"test 3\\" got checked for the first time! 🎉 +", + " + / | / _ _ _ + '-.ooo.-' | |__ ___| |_| |_ ___ _ __ ___ _ __ +---ooooo--- | '_ // _ / __| __/ _ / '__/ _ / '__| + .-'ooo'-. | |_)| __/ |_| || __/ | | __/ | + / | / |_.__//___|/__|/__/___|_| /___|_| + +🎉 Betterer (0ms): 3 tests done! +✅ test 1: \\"test 1\\" got checked for the first time! 🎉 +✅ test 2: \\"test 2\\" got checked for the first time! 🎉 +✅ test 3: \\"test 3\\" got checked for the first time! 🎉 + +3 tests got checked. 🤔 +3 tests got checked for the first time! 🎉 +", + " + / | / _ _ _ + '-.ooo.-' | |__ ___| |_| |_ ___ _ __ ___ _ __ +---ooooo--- | '_ // _ / __| __/ _ / '__/ _ / '__| + .-'ooo'-. | |_)| __/ |_| || __/ | | __/ | + / | / |_.__//___|/__|/__/___|_| /___|_| +", + " + / | / _ _ _ + '-.ooo.-' | |__ ___| |_| |_ ___ _ __ ___ _ __ +---ooooo--- | '_ // _ / __| __/ _ / '__/ _ / '__| + .-'ooo'-. | |_)| __/ |_| || __/ | | __/ | + / | / |_.__//___|/__|/__/___|_| /___|_| + +🌟 Betterer (0ms): +", + " + / | / _ _ _ + '-.ooo.-' | |__ ___| |_| |_ ___ _ __ ___ _ __ +---ooooo--- | '_ // _ / __| __/ _ / '__/ _ / '__| + .-'ooo'-. | |_)| __/ |_| || __/ | | __/ | + / | / |_.__//___|/__|/__/___|_| /___|_| + +🎉 Betterer (0ms): 3 tests done! +✅ test 1: \\"test 1\\" got skipped. 🚫 +✅ test 2: \\"test 2\\" stayed the same. 😐 +✅ test 3: \\"test 3\\" stayed the same. 😐 +", + " + / | / _ _ _ + '-.ooo.-' | |__ ___| |_| |_ ___ _ __ ___ _ __ +---ooooo--- | '_ // _ / __| __/ _ / '__/ _ / '__| + .-'ooo'-. | |_)| __/ |_| || __/ | | __/ | + / | / |_.__//___|/__|/__/___|_| /___|_| + +🎉 Betterer (0ms): 3 tests done! +✅ test 1: \\"test 1\\" got skipped. 🚫 +✅ test 2: \\"test 2\\" stayed the same. 😐 +✅ test 3: \\"test 3\\" stayed the same. 😐 + +2 tests got checked. 🤔 +2 tests stayed the same. 😐 +1 test got skipped. 🚫 +", + " + / | / _ _ _ + '-.ooo.-' | |__ ___| |_| |_ ___ _ __ ___ _ __ +---ooooo--- | '_ // _ / __| __/ _ / '__/ _ / '__| + .-'ooo'-. | |_)| __/ |_| || __/ | | __/ | + / | / |_.__//___|/__|/__/___|_| /___|_| +", + " + / | / _ _ _ + '-.ooo.-' | |__ ___| |_| |_ ___ _ __ ___ _ __ +---ooooo--- | '_ // _ / __| __/ _ / '__/ _ / '__| + .-'ooo'-. | |_)| __/ |_| || __/ | | __/ | + / | / |_.__//___|/__|/__/___|_| /___|_| + +🌟 Betterer (0ms): +", + " + / | / _ _ _ + '-.ooo.-' | |__ ___| |_| |_ ___ _ __ ___ _ __ +---ooooo--- | '_ // _ / __| __/ _ / '__/ _ / '__| + .-'ooo'-. | |_)| __/ |_| || __/ | | __/ | + / | / |_.__//___|/__|/__/___|_| /___|_| + +🎉 Betterer (0ms): 3 tests done! +✅ test 1: \\"test 1\\" stayed the same. 😐 +✅ test 2: \\"test 2\\" got skipped. 🚫 +✅ test 3: \\"test 3\\" got skipped. 🚫 +", + " + / | / _ _ _ + '-.ooo.-' | |__ ___| |_| |_ ___ _ __ ___ _ __ +---ooooo--- | '_ // _ / __| __/ _ / '__/ _ / '__| + .-'ooo'-. | |_)| __/ |_| || __/ | | __/ | + / | / |_.__//___|/__|/__/___|_| /___|_| + +🎉 Betterer (0ms): 3 tests done! +✅ test 1: \\"test 1\\" stayed the same. 😐 +✅ test 2: \\"test 2\\" got skipped. 🚫 +✅ test 3: \\"test 3\\" got skipped. 🚫 + +1 test got checked. 🤔 +1 test stayed the same. 😐 +2 tests got skipped. 🚫 +", +] +`; diff --git a/test/cli/filter-negative.spec.ts b/test/cli/filter-negative.spec.ts new file mode 100644 index 000000000..d40f965c1 --- /dev/null +++ b/test/cli/filter-negative.spec.ts @@ -0,0 +1,55 @@ +import { startΔ } from '@betterer/cli'; + +import { createFixture } from '../fixture'; + +const ARGV = ['node', './bin/betterer']; + +describe('betterer cli', () => { + it('should filter tests by name with negation', async () => { + const { logs, paths, cleanup, runNames } = await createFixture( + 'filter-negative', + { + '.betterer.js': ` +const { BettererTest } = require('@betterer/betterer'); +const { bigger } = require('@betterer/constraints'); + +module.exports = { + 'test 1': () => new BettererTest({ + test: () => 0, + constraint: bigger + }), + 'test 2': () => new BettererTest({ + test: () => 0, + constraint: bigger + }), + 'test 3': () => new BettererTest({ + test: () => 0, + constraint: bigger + }) +}; + ` + }, + { + logFilters: [/: running /, /running.../] + } + ); + + const fixturePath = paths.cwd; + + const firstRun = await startΔ(fixturePath, ARGV, false); + + expect(runNames(firstRun.ran)).toEqual(['test 1', 'test 2', 'test 3']); + + const secondRun = await startΔ(fixturePath, [...ARGV, '--filter', '!1'], false); + + expect(runNames(secondRun.ran)).toEqual(['test 2', 'test 3']); + + const thirdRun = await startΔ(fixturePath, [...ARGV, '--filter', 'test', '--filter', '![2|3]'], false); + + expect(runNames(thirdRun.ran)).toEqual(['test 1']); + + expect(logs).toMatchSnapshot(); + + await cleanup(); + }); +});