Skip to content

Commit

Permalink
Consolidate testPathPatterns logic
Browse files Browse the repository at this point in the history
  • Loading branch information
brandonchinn178 committed Sep 22, 2023
1 parent 317d079 commit 86993d4
Show file tree
Hide file tree
Showing 8 changed files with 217 additions and 116 deletions.
54 changes: 0 additions & 54 deletions packages/jest-config/src/__tests__/normalize.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1630,60 +1630,6 @@ describe('testPathPattern', () => {

expect(options.testPathPattern).toBe('a/b|c/d');
});

describe('posix', () => {
it('should not escape the pattern', async () => {
const argv = {
[opt.property]: ['a\\/b', 'a/b', 'a\\b', 'a\\\\b'],
} as Config.Argv;
const {options} = await normalize(initialOptions, argv);

expect(options.testPathPattern).toBe('a\\/b|a/b|a\\b|a\\\\b');
});
});

describe('win32', () => {
beforeEach(() => {
jest.mock(
'path',
() => jest.requireActual<typeof import('path')>('path').win32,
);
(
require('jest-resolve') as typeof import('jest-resolve')
).default.findNodeModule = findNodeModule;
});

afterEach(() => {
jest.resetModules();
});

it('preserves any use of "\\"', async () => {
const argv = {[opt.property]: ['a\\b', 'c\\\\d']} as Config.Argv;
const {options} = await (
require('../normalize') as typeof import('../normalize')
).default(initialOptions, argv);

expect(options.testPathPattern).toBe('a\\b|c\\\\d');
});

it('replaces POSIX path separators', async () => {
const argv = {[opt.property]: ['a/b']} as Config.Argv;
const {options} = await (
require('../normalize') as typeof import('../normalize')
).default(initialOptions, argv);

expect(options.testPathPattern).toBe('a\\\\b');
});

it('replaces POSIX paths in multiple args', async () => {
const argv = {[opt.property]: ['a/b', 'c/d']} as Config.Argv;
const {options} = await (
require('../normalize') as typeof import('../normalize')
).default(initialOptions, argv);

expect(options.testPathPattern).toBe('a\\\\b|c\\\\d');
});
});
});
}

Expand Down
35 changes: 0 additions & 35 deletions packages/jest-config/src/__tests__/validatePattern.test.ts

This file was deleted.

13 changes: 5 additions & 8 deletions packages/jest-config/src/normalize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import Resolver, {
resolveWatchPlugin,
} from 'jest-resolve';
import {
TestPathPatterns,
clearLine,
replacePathSepForGlob,
requireOrImportModule,
Expand Down Expand Up @@ -49,7 +50,6 @@ import {
replaceRootDirInPath,
resolve,
} from './utils';
import validatePattern from './validatePattern';

const ERROR = `${BULLET}Validation Error`;
const PRESET_EXTENSIONS = ['.json', '.js', '.cjs', '.mjs'];
Expand Down Expand Up @@ -401,14 +401,11 @@ const buildTestPathPattern = (argv: Config.Argv): string => {
patterns.push(...argv.testPathPattern);
}

const replacePosixSep = (pattern: string) =>
path.sep === '/' ? pattern : pattern.replace(/\//g, '\\\\');

const testPathPattern = patterns.map(replacePosixSep).join('|');
if (validatePattern(testPathPattern)) {
return testPathPattern;
const testPathPatterns = new TestPathPatterns(patterns);
if (testPathPatterns.isValid()) {
return testPathPatterns.regexString;
} else {
showTestPathPatternError(testPathPattern);
showTestPathPatternError(testPathPatterns.regexString);
return '';
}
};
Expand Down
19 changes: 0 additions & 19 deletions packages/jest-config/src/validatePattern.ts

This file was deleted.

74 changes: 74 additions & 0 deletions packages/jest-util/src/TestPathPatterns.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

import {replacePathSepForRegex} from 'jest-regex-util';

export default class TestPathPatterns {
readonly patterns: Array<string>;

private _regexString: string | null = null;

constructor(patterns: Array<string>) {
this.patterns = patterns;
}

get regexString(): string {
if (this._regexString !== null) {
return this._regexString;
}
const regexString = this.patterns
.map(replacePathSepForRegex)
.join('|');
this._regexString = regexString;
return regexString;
}

private get regex(): RegExp {
return new RegExp(this.regexString, 'i');
}

/**
* Return true if there are any patterns.
*/
isSet(): boolean {
return this.patterns.length > 0;
}

/**
* Return true if the patterns form a valid regex.
*/
isValid(): boolean {
try {
// @ts-expect-error noUnusedLocals
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const _ = this.regex;
return true;
} catch {
return false;
}
}

/**
* Return true if the given ABSOLUTE path matches the patterns.
*
* Throws an error if the patterns form an invalid regex (see `isValid`).
*/
isMatch(path: string): boolean {
return this.regex.test(path);
}

/**
* Return a human-friendly version of the pattern regex.
*
* Does no normalization or anything, just a naive joining of the regex,
* for simplicity.
*/
toPretty(): string {
const regex = this.patterns.map(p => p.replace(/\//g, '\\/')).join('|');
return `/${regex}/i`;
}
}
134 changes: 134 additions & 0 deletions packages/jest-util/src/__tests__/TestPathPatterns.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

import type * as path from 'path';
import TestPathPatterns from '../TestPathPatterns';

const mockSep = jest.fn();
jest.mock('path', () => {
return {
...(jest.requireActual('path') as typeof path),
get sep() {
return mockSep() || '/';
},
};
});
beforeEach(() => {
jest.resetAllMocks();
});

describe('TestPathPatterns', () => {
describe('isSet', () => {
it('returns false if no patterns specified', () => {
const testPathPatterns = new TestPathPatterns([]);
expect(testPathPatterns.isSet()).toBe(false);
});

it('returns true if patterns specified', () => {
const testPathPatterns = new TestPathPatterns(['a']);
expect(testPathPatterns.isSet()).toBe(true);
});
});

describe('isValid', () => {
it('returns true for empty patterns', () => {
const testPathPatterns = new TestPathPatterns([]);
expect(testPathPatterns.isValid()).toBe(true);
});

it('returns true for valid patterns', () => {
const testPathPatterns = new TestPathPatterns(['abc+', 'z.*']);
expect(testPathPatterns.isValid()).toBe(true);
});

it('returns false for at least one invalid pattern', () => {
const testPathPatterns = new TestPathPatterns(['abc+', '(', 'z.*']);
expect(testPathPatterns.isValid()).toBe(false);
});
});

describe('isMatch', () => {
it('returns true with no patterns', () => {
const testPathPatterns = new TestPathPatterns([]);
expect(testPathPatterns.isMatch('/a/b')).toBe(true);
});

it('returns true for same path', () => {
const testPathPatterns = new TestPathPatterns(['/a/b']);
expect(testPathPatterns.isMatch('/a/b')).toBe(true);
});

it('returns true for same path with case insensitive', () => {
const testPathPatternsUpper = new TestPathPatterns(['/A/B']);
expect(testPathPatternsUpper.isMatch('/a/b')).toBe(true);
expect(testPathPatternsUpper.isMatch('/A/B')).toBe(true);

const testPathPatternsLower = new TestPathPatterns(['/a/b']);
expect(testPathPatternsLower.isMatch('/A/B')).toBe(true);
expect(testPathPatternsLower.isMatch('/a/b')).toBe(true);
});

it('returns true for contained path', () => {
const testPathPatterns = new TestPathPatterns(['b/c']);
expect(testPathPatterns.isMatch('/a/b/c/d')).toBe(true);
});

it('returns true for partial file match', () => {
const testPathPatterns = new TestPathPatterns(['aaa']);
expect(testPathPatterns.isMatch('/foo/..aaa..')).toBe(true);
expect(testPathPatterns.isMatch('/foo/..aaa')).toBe(true);
expect(testPathPatterns.isMatch('/foo/aaa..')).toBe(true);
});

it('returns true for path suffix', () => {
const testPathPatterns = new TestPathPatterns(['c/d']);
expect(testPathPatterns.isMatch('/a/b/c/d')).toBe(true);
});

it('returns true if regex matches', () => {
const testPathPatterns = new TestPathPatterns(['ab*c?']);

expect(testPathPatterns.isMatch('/foo/a')).toBe(true);
expect(testPathPatterns.isMatch('/foo/ab')).toBe(true);
expect(testPathPatterns.isMatch('/foo/abb')).toBe(true);
expect(testPathPatterns.isMatch('/foo/ac')).toBe(true);
expect(testPathPatterns.isMatch('/foo/abc')).toBe(true);
expect(testPathPatterns.isMatch('/foo/abbc')).toBe(true);

expect(testPathPatterns.isMatch('/foo/bc')).toBe(false);
});

it('returns true if match any paths', () => {
const testPathPatterns = new TestPathPatterns(['a/b', 'c/d']);

expect(testPathPatterns.isMatch('/foo/a/b')).toBe(true);
expect(testPathPatterns.isMatch('/foo/c/d')).toBe(true);

expect(testPathPatterns.isMatch('/foo/a')).toBe(false);
expect(testPathPatterns.isMatch('/foo/b/c')).toBe(false);
});

it('does not normalize Windows paths on POSIX', () => {
mockSep.mockReturnValue('/');
const testPathPatterns = new TestPathPatterns(['a\\z', 'a\\\\z']);
expect(testPathPatterns.isMatch('/foo/a/z')).toBe(false);
});

it('normalizes paths for Windows', () => {
mockSep.mockReturnValue('\\');
const testPathPatterns = new TestPathPatterns(['a/b']);
expect(testPathPatterns.isMatch('\\foo\\a\\b')).toBe(true);
});
});

describe('toPretty', () => {
it('renders a human-readable string', () => {
const testPathPatterns = new TestPathPatterns(['a/b', 'c/d']);
expect(testPathPatterns.toPretty()).toMatchSnapshot();
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`TestPathPatterns toPretty renders a human-readable string 1`] = `"/a\\/b|c\\/d/i"`;
1 change: 1 addition & 0 deletions packages/jest-util/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export {default as deepCyclicCopy} from './deepCyclicCopy';
export {default as convertDescriptorToString} from './convertDescriptorToString';
export {specialChars};
export {default as replacePathSepForGlob} from './replacePathSepForGlob';
export {default as TestPathPatterns} from './TestPathPatterns';
export {default as testPathPatternToRegExp} from './testPathPatternToRegExp';
export {default as globsToMatcher} from './globsToMatcher';
export {preRunMessage};
Expand Down

0 comments on commit 86993d4

Please sign in to comment.