Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix absolute path check for Windows #15235

Merged
merged 1 commit into from
Aug 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
});
});
});
Loading