From 9b08aa05f4879b180b72672e8d4c4f3bbc8036b9 Mon Sep 17 00:00:00 2001 From: Brandon Chinn Date: Thu, 8 Aug 2024 00:29:39 -0700 Subject: [PATCH] Fix absolute path check for Windows (#15235) --- packages/jest-pattern/src/TestPathPatterns.ts | 68 ++++++++----------- .../src/__tests__/TestPathPatterns.test.ts | 57 ++++++++++++++-- 2 files changed, 81 insertions(+), 44 deletions(-) diff --git a/packages/jest-pattern/src/TestPathPatterns.ts b/packages/jest-pattern/src/TestPathPatterns.ts index 26b0f4132117..4ed7e3875252 100644 --- a/packages/jest-pattern/src/TestPathPatterns.ts +++ b/packages/jest-pattern/src/TestPathPatterns.ts @@ -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) {} @@ -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'); } /** @@ -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; @@ -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; } /** diff --git a/packages/jest-pattern/src/__tests__/TestPathPatterns.test.ts b/packages/jest-pattern/src/__tests__/TestPathPatterns.test.ts index ced625fb17ed..4d01f4eac57c 100644 --- a/packages/jest-pattern/src/__tests__/TestPathPatterns.test.ts +++ b/packages/jest-pattern/src/__tests__/TestPathPatterns.test.ts @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -import type * as path from 'path'; +import * as path from 'path'; import { TestPathPatterns, TestPathPatternsExecutor, @@ -13,16 +13,36 @@ import { } 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: ''}; @@ -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); @@ -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); @@ -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); }); }); });