Skip to content

Commit

Permalink
Rewrite TestPathPatterns to better handle Windows
Browse files Browse the repository at this point in the history
  • Loading branch information
brandonchinn178 committed Aug 7, 2024
1 parent 213c3b2 commit 3835811
Show file tree
Hide file tree
Showing 2 changed files with 81 additions and 44 deletions.
68 changes: 30 additions & 38 deletions packages/jest-pattern/src/TestPathPatterns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
* LICENSE file in the root directory of this source tree.
*/

import {escapePathForRegex, replacePathSepForRegex} from 'jest-regex-util';
import * as path from 'path';
import {replacePathSepForRegex} from 'jest-regex-util';

export class TestPathPatterns {
constructor(readonly patterns: Array<string>) {}
Expand Down Expand Up @@ -58,45 +59,13 @@ export type TestPathPatternsExecutorOptions = {
};

export class TestPathPatternsExecutor {
private _regexString: string | null = null;

constructor(
readonly patterns: TestPathPatterns,
private readonly options: TestPathPatternsExecutorOptions,
) {}

private get regexString(): string {
if (this._regexString !== null) {
return this._regexString;
}

const rootDir = this.options.rootDir.replace(/\/*$/, '/');
const rootDirRegex = escapePathForRegex(rootDir);

const regexString = this.patterns.patterns
.map(p => {
// absolute paths passed on command line should stay same
if (p.startsWith('/')) {
return p;
}

// explicit relative paths should resolve against rootDir
if (p.startsWith('./')) {
return p.replace(/^\.\//, rootDirRegex);
}

// all other patterns should only match the relative part of the test
return `${rootDirRegex}(.*)?${p}`;
})
.map(replacePathSepForRegex)
.join('|');

this._regexString = regexString;
return regexString;
}

private toRegex(): RegExp {
return new RegExp(this.regexString, 'i');
private toRegex(s: string): RegExp {
return new RegExp(s, 'i');
}

/**
Expand All @@ -111,7 +80,9 @@ export class TestPathPatternsExecutor {
*/
isValid(): boolean {
try {
this.toRegex();
for (const p of this.patterns.patterns) {
this.toRegex(p);
}
return true;
} catch {
return false;
Expand All @@ -123,8 +94,29 @@ export class TestPathPatternsExecutor {
*
* Throws an error if the patterns form an invalid regex (see `validate`).
*/
isMatch(path: string): boolean {
return this.toRegex().test(path);
isMatch(absPath: string): boolean {
const relPath = path.relative(this.options.rootDir || '/', absPath);

if (this.patterns.patterns.length === 0) {
return true;
}

for (const p of this.patterns.patterns) {
const pathToTest = path.isAbsolute(p) ? absPath : relPath;

// special case: ./foo.spec.js (and .\foo.spec.js on Windows) should
// match /^foo.spec.js/ after stripping root dir
let regexStr = p.replace(/^\.\//, '^');
if (path.sep === '\\') {
regexStr = regexStr.replace(/^\.\\/, '^');
}

regexStr = replacePathSepForRegex(regexStr);
if (this.toRegex(regexStr).test(pathToTest)) {
return true;
}
}
return false;
}

/**
Expand Down
57 changes: 51 additions & 6 deletions packages/jest-pattern/src/__tests__/TestPathPatterns.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,44 @@
* LICENSE file in the root directory of this source tree.
*/

import type * as path from 'path';
import * as path from 'path';
import {
TestPathPatterns,
TestPathPatternsExecutor,
type TestPathPatternsExecutorOptions,
} from '../TestPathPatterns';

const mockSep: jest.Mock<() => string> = jest.fn();
const mockIsAbsolute: jest.Mock<(p: string) => boolean> = jest.fn();
const mockRelative: jest.Mock<(from: string, to: string) => string> = jest.fn();
jest.mock('path', () => {
const actualPath = jest.requireActual('path');
return {
...jest.requireActual('path'),
...actualPath,
isAbsolute(p) {
return mockIsAbsolute(p) || actualPath.isAbsolute(p);
},
relative(from, to) {
return mockRelative(from, to) || actualPath.relative(from, to);
},
get sep() {
return mockSep() || '/';
return mockSep() || actualPath.sep;
},
} as typeof path;
});
const forcePosix = () => {
mockSep.mockReturnValue(path.posix.sep);
mockIsAbsolute.mockImplementation(path.posix.isAbsolute);
mockRelative.mockImplementation(path.posix.relative);
};
const forceWindows = () => {
mockSep.mockReturnValue(path.win32.sep);
mockIsAbsolute.mockImplementation(path.win32.isAbsolute);
mockRelative.mockImplementation(path.win32.relative);
};
beforeEach(() => {
jest.resetAllMocks();
forcePosix();
});

const config = {rootDir: ''};
Expand Down Expand Up @@ -124,6 +144,22 @@ describe('TestPathPatternsExecutor', () => {
expect(testPathPatterns.isMatch('/a/b/c')).toBe(true);
});

it('returns true for explicit relative path for Windows with ./', () => {
forceWindows();
const testPathPatterns = makeExecutor(['./b/c'], {
rootDir: 'C:\\a',
});
expect(testPathPatterns.isMatch('C:\\a\\b\\c')).toBe(true);
});

it('returns true for explicit relative path for Windows with .\\', () => {
forceWindows();
const testPathPatterns = makeExecutor(['.\\b\\c'], {
rootDir: 'C:\\a',
});
expect(testPathPatterns.isMatch('C:\\a\\b\\c')).toBe(true);
});

it('returns true for partial file match', () => {
const testPathPatterns = makeExecutor(['aaa'], config);
expect(testPathPatterns.isMatch('/foo/..aaa..')).toBe(true);
Expand Down Expand Up @@ -158,12 +194,21 @@ describe('TestPathPatternsExecutor', () => {
});

it('matches absolute paths regardless of rootDir', () => {
forcePosix();
const testPathPatterns = makeExecutor(['/a/b'], {
rootDir: '/foo/bar',
});
expect(testPathPatterns.isMatch('/a/b')).toBe(true);
});

it('matches absolute paths for Windows', () => {
forceWindows();
const testPathPatterns = makeExecutor(['C:\\a\\b'], {
rootDir: 'C:\\foo\\bar',
});
expect(testPathPatterns.isMatch('C:\\a\\b')).toBe(true);
});

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

Expand All @@ -175,15 +220,15 @@ describe('TestPathPatternsExecutor', () => {
});

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

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

0 comments on commit 3835811

Please sign in to comment.