diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d58d366357f..7dc5d3e317b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,8 @@ - `[expect]` Match symbols and bigints in `any()` ([#10223](https://github.com/facebook/jest/pull/10223)) - `[jest-changed-files]` Use `git diff` instead of `git log` for `--changedSince` ([#10155](https://github.com/facebook/jest/pull/10155)) -- `[jest-console]` Add missing console.timeLog for compatability with Node ([#10209](https://github.com/facebook/jest/pull/10209)) +- `[jest-console]` Add missing `console.timeLog` for compatibility with Node ([#10209](https://github.com/facebook/jest/pull/10209)) +- `[jest-haste-map]` Check `find` binary supports the `-iname` parameter ([#10308](https://github.com/facebook/jest/pull/10308)) - `[jest-snapshot]` Strip added indentation for inline error snapshots ([#10217](https://github.com/facebook/jest/pull/10217)) ### Chore & Maintenance diff --git a/packages/jest-haste-map/package.json b/packages/jest-haste-map/package.json index 76e34ab3ea23..1ffbf315b3f7 100644 --- a/packages/jest-haste-map/package.json +++ b/packages/jest-haste-map/package.json @@ -21,8 +21,7 @@ "jest-worker": "^26.1.0", "micromatch": "^4.0.2", "sane": "^4.0.3", - "walker": "^1.0.7", - "which": "^2.0.2" + "walker": "^1.0.7" }, "devDependencies": { "@jest/test-utils": "^26.0.0", @@ -30,7 +29,7 @@ "@types/fb-watchman": "^2.0.0", "@types/micromatch": "^4.0.0", "@types/sane": "^2.0.0", - "@types/which": "^1.3.2" + "slash": "^3.0.0" }, "optionalDependencies": { "fsevents": "^2.1.2" diff --git a/packages/jest-haste-map/src/crawlers/__tests__/node.test.js b/packages/jest-haste-map/src/crawlers/__tests__/node.test.js index c972f0da6dd1..467c0acb9195 100644 --- a/packages/jest-haste-map/src/crawlers/__tests__/node.test.js +++ b/packages/jest-haste-map/src/crawlers/__tests__/node.test.js @@ -8,12 +8,15 @@ 'use strict'; -import {skipSuiteOnWindows} from '@jest/test-utils'; - jest.mock('child_process', () => ({ spawn: jest.fn((cmd, args) => { let closeCallback; return { + on: jest.fn().mockImplementation((event, callback) => { + if (event === 'exit') { + callback(mockSpawnExit, null); + } + }), stdout: { on: jest.fn().mockImplementation((event, callback) => { if (event === 'data') { @@ -34,6 +37,7 @@ jest.mock('child_process', () => ({ let mockHasReaddirWithFileTypesSupport = false; jest.mock('graceful-fs', () => { + const slash = require('slash'); let mtime = 32; const size = 42; const stat = (path, callback) => { @@ -41,10 +45,10 @@ jest.mock('graceful-fs', () => { () => callback(null, { isDirectory() { - return path.endsWith('/directory'); + return slash(path).endsWith('/directory'); }, isSymbolicLink() { - return path.endsWith('symlink'); + return slash(path).endsWith('symlink'); }, mtime: { getTime() { @@ -70,7 +74,7 @@ jest.mock('graceful-fs', () => { } if (mockHasReaddirWithFileTypesSupport) { - if (dir === '/project/fruits') { + if (slash(dir) === '/project/fruits') { setTimeout( () => callback(null, [ @@ -92,7 +96,7 @@ jest.mock('graceful-fs', () => { ]), 0, ); - } else if (dir === '/project/fruits/directory') { + } else if (slash(dir) === '/project/fruits/directory') { setTimeout( () => callback(null, [ @@ -104,18 +108,18 @@ jest.mock('graceful-fs', () => { ]), 0, ); - } else if (dir == '/error') { + } else if (slash(dir) == '/error') { setTimeout(() => callback({code: 'ENOTDIR'}, undefined), 0); } } else { - if (dir === '/project/fruits') { + if (slash(dir) === '/project/fruits') { setTimeout( () => callback(null, ['directory', 'tomato.js', 'symlink']), 0, ); - } else if (dir === '/project/fruits/directory') { + } else if (slash(dir) === '/project/fruits/directory') { setTimeout(() => callback(null, ['strawberry.js']), 0); - } else if (dir == '/error') { + } else if (slash(dir) == '/error') { setTimeout(() => callback({code: 'ENOTDIR'}, undefined), 0); } } @@ -124,35 +128,32 @@ jest.mock('graceful-fs', () => { }; }); -jest.mock('which', () => jest.fn().mockResolvedValue()); - const pearMatcher = path => /pear/.test(path); -const createMap = obj => new Map(Object.keys(obj).map(key => [key, obj[key]])); +const normalize = path => + process.platform === 'win32' ? path.replace(/\//g, '\\') : path; +const createMap = obj => + new Map(Object.keys(obj).map(key => [normalize(key), obj[key]])); const rootDir = '/project'; let mockResponse; +let mockSpawnExit; let nodeCrawl; let childProcess; describe('node crawler', () => { - skipSuiteOnWindows(); - beforeEach(() => { jest.resetModules(); - // Remove the "process.platform" property descriptor so it can be writable. - delete process.platform; - mockResponse = [ '/project/fruits/pear.js', '/project/fruits/strawberry.js', '/project/fruits/tomato.js', ].join('\n'); + + mockSpawnExit = 0; }); it('crawls for files based on patterns', () => { - process.platform = 'linux'; - childProcess = require('child_process'); nodeCrawl = require('../node'); @@ -203,8 +204,6 @@ describe('node crawler', () => { }); it('updates only changed files', () => { - process.platform = 'linux'; - nodeCrawl = require('../node'); // In this test sample, strawberry is changed and tomato is unchanged @@ -229,15 +228,13 @@ describe('node crawler', () => { ); // Make sure it is the *same* unchanged object. - expect(hasteMap.files.get('fruits/tomato.js')).toBe(tomato); + expect(hasteMap.files.get(normalize('fruits/tomato.js'))).toBe(tomato); expect(removedFiles).toEqual(new Map()); }); }); it('returns removed files', () => { - process.platform = 'linux'; - nodeCrawl = require('../node'); // In this test sample, previouslyExisted was present before and will not be @@ -269,8 +266,10 @@ describe('node crawler', () => { }); }); - it('uses node fs APIs on windows', () => { - process.platform = 'win32'; + it('uses node fs APIs with incompatible find binary', () => { + mockResponse = ''; + mockSpawnExit = 1; + childProcess = require('child_process'); nodeCrawl = require('../node'); @@ -283,6 +282,7 @@ describe('node crawler', () => { rootDir, roots: ['/project/fruits'], }).then(({hasteMap, removedFiles}) => { + expect(childProcess.spawn).lastCalledWith('find', ['.', '-iname', "''"]); expect(hasteMap.files).toEqual( createMap({ 'fruits/directory/strawberry.js': ['', 33, 42, 0, '', null], @@ -293,11 +293,11 @@ describe('node crawler', () => { }); }); - it('uses node fs APIs on Unix based OS without find binary', () => { - process.platform = 'linux'; - const which = require('which'); - which.mockReturnValueOnce(Promise.reject()); - + it('uses node fs APIs without find binary', () => { + childProcess = require('child_process'); + childProcess.spawn.mockImplementationOnce(() => { + throw new Error(); + }); nodeCrawl = require('../node'); return nodeCrawl({ @@ -316,13 +316,11 @@ describe('node crawler', () => { }), ); expect(removedFiles).toEqual(new Map()); - expect(which).toBeCalledWith('find'); }); }); it('uses node fs APIs if "forceNodeFilesystemAPI" is set to true, regardless of platform', () => { - process.platform = 'linux'; - + childProcess = require('child_process'); nodeCrawl = require('../node'); const files = new Map(); @@ -334,6 +332,7 @@ describe('node crawler', () => { rootDir, roots: ['/project/fruits'], }).then(({hasteMap, removedFiles}) => { + expect(childProcess.spawn).toHaveBeenCalledTimes(0); expect(hasteMap.files).toEqual( createMap({ 'fruits/directory/strawberry.js': ['', 33, 42, 0, '', null], @@ -345,8 +344,6 @@ describe('node crawler', () => { }); it('completes with empty roots', () => { - process.platform = 'win32'; - nodeCrawl = require('../node'); const files = new Map(); @@ -364,14 +361,13 @@ describe('node crawler', () => { }); it('completes with fs.readdir throwing an error', () => { - process.platform = 'win32'; - nodeCrawl = require('../node'); const files = new Map(); return nodeCrawl({ data: {files}, extensions: ['js'], + forceNodeFilesystemAPI: true, ignore: pearMatcher, rootDir, roots: ['/error'], diff --git a/packages/jest-haste-map/src/crawlers/node.ts b/packages/jest-haste-map/src/crawlers/node.ts index 0266816d91fb..be6bcae43b05 100644 --- a/packages/jest-haste-map/src/crawlers/node.ts +++ b/packages/jest-haste-map/src/crawlers/node.ts @@ -8,7 +8,6 @@ import * as path from 'path'; import {spawn} from 'child_process'; import * as fs from 'graceful-fs'; -import which = require('which'); import H from '../constants'; import * as fastPath from '../lib/fast_path'; import type { @@ -25,13 +24,22 @@ type Callback = (result: Result) => void; async function hasNativeFindSupport( forceNodeFilesystemAPI: boolean, ): Promise { - if (forceNodeFilesystemAPI || process.platform === 'win32') { + if (forceNodeFilesystemAPI) { return false; } try { - await which('find'); - return true; + return await new Promise(resolve => { + // Check the find binary supports the non-POSIX -iname parameter. + const args = ['.', '-iname', "''"]; + const child = spawn('find', args); + child.on('error', () => { + resolve(false); + }); + child.on('exit', code => { + resolve(code === 0); + }); + }); } catch { return false; } diff --git a/yarn.lock b/yarn.lock index 25e98d817b6d..4569c6275937 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11208,7 +11208,6 @@ fsevents@^1.2.7: "@types/micromatch": ^4.0.0 "@types/node": "*" "@types/sane": ^2.0.0 - "@types/which": ^1.3.2 anymatch: ^3.0.3 fb-watchman: ^2.0.0 fsevents: ^2.1.2 @@ -11218,8 +11217,8 @@ fsevents@^1.2.7: jest-worker: ^26.1.0 micromatch: ^4.0.2 sane: ^4.0.3 + slash: ^3.0.0 walker: ^1.0.7 - which: ^2.0.2 dependenciesMeta: fsevents: optional: true