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

feat(jest-haste-map): handle injected scm clocks #10966

Merged
merged 4 commits into from Dec 22, 2020
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -15,6 +15,7 @@
- `[jest-runner]` [**BREAKING**] Run transforms over `testRunnner` ([#8823](https://github.com/facebook/jest/pull/8823))
- `[jest-runtime, jest-transform]` share `cacheFS` between runtime and transformer ([#10901](https://github.com/facebook/jest/pull/10901))
- `[jest-transform]` Pass config options defined in Jest's config to transformer's `process` and `getCacheKey` functions ([#10926](https://github.com/facebook/jest/pull/10926))
- `[jest-haste-map]` Handle injected scm clocks ([#10966](https://github.com/facebook/jest/pull/10966))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you make it alphabetical? 😀


### Fixes

Expand Down
85 changes: 85 additions & 0 deletions packages/jest-haste-map/src/crawlers/__tests__/watchman.test.js
Expand Up @@ -589,4 +589,89 @@ describe('watchman watch', () => {
expect(calls[0][0]).toEqual(['list-capabilities']);
expect(calls[2][0][2].fields).not.toContain('content.sha1hex');
});

test('source control query', async () => {
scotthovestadt marked this conversation as resolved.
Show resolved Hide resolved
mockResponse = {
'list-capabilities': {
[undefined]: {
capabilities: ['field-content.sha1hex'],
},
},
query: {
[ROOT_MOCK]: {
clock: {
clock: 'c:1608612057:79675:1:139410',
scm: {
mergebase: 'master',
'mergebase-with': 'master',
},
},
files: [
{
exists: true,
mtime_ms: {toNumber: () => 42},
name: 'fruits/kiwi.js',
size: 40,
},
{
exists: false,
mtime_ms: null,
name: 'fruits/tomato.js',
size: 0,
},
],
// Watchman is going to tell us that we have a fresh instance.
is_fresh_instance: true,
version: '4.5.0',
},
},
'watch-project': WATCH_PROJECT_MOCK,
};

// Start with a source-control clock.
const clocks = createMap({
'': {scm: {'mergebase-with': 'master'}},
});

return watchmanCrawl({
data: {
clocks,
files: mockFiles,
},
extensions: ['js', 'json'],
ignore: pearMatcher,
rootDir: ROOT_MOCK,
roots: ROOTS,
}).then(({changedFiles, hasteMap, removedFiles}) => {
// The object was reused.
expect(hasteMap.files).toBe(mockFiles);

// Transformed into a normal clock.
expect(hasteMap.clocks).toEqual(
createMap({
'': 'c:1608612057:79675:1:139410',
}),
);

expect(changedFiles).toEqual(
createMap({
[KIWI_RELATIVE]: ['', 42, 40, 0, '', null],
}),
);

expect(hasteMap.files).toEqual(
createMap({
[KIWI_RELATIVE]: ['', 42, 40, 0, '', null],
[MELON_RELATIVE]: ['', 33, 43, 0, '', null],
[STRAWBERRY_RELATIVE]: ['', 30, 40, 0, '', null],
}),
);

expect(removedFiles).toEqual(
createMap({
[TOMATO_RELATIVE]: ['', 31, 41, 0, '', null],
}),
);
});
});
});
96 changes: 74 additions & 22 deletions packages/jest-haste-map/src/crawlers/watchman.ts
Expand Up @@ -20,6 +20,34 @@ import type {

type WatchmanRoots = Map<string, Array<string>>;

type WatchmanListCapabilitiesResponse = {
capabilities: Array<string>;
};

type WatchmanWatchProjectResponse = {
watch: string;
relative_path: string;
};

type WatchmanQueryResponse = {
warning?: string;
is_fresh_instance: boolean;
version: string;
clock:
| string
| {
scm: {'mergebase-with': string; mergebase: string};
clock: string;
};
files: Array<{
name: string;
exists: boolean;
mtime_ms: number | {toNumber: () => number};
size: number;
'content.sha1hex'?: string;
}>;
};

const watchmanURL = 'https://facebook.github.io/watchman/docs/troubleshooting';

function WatchmanError(error: Error): Error {
Expand Down Expand Up @@ -49,16 +77,17 @@ export = async function watchmanCrawl(
let clientError;
client.on('error', error => (clientError = WatchmanError(error)));

// TODO: type better than `any`
const cmd = (...args: Array<any>): Promise<any> =>
const cmd = <T>(...args: Array<any>): Promise<T> =>
new Promise((resolve, reject) =>
client.command(args, (error, result) =>
error ? reject(WatchmanError(error)) : resolve(result),
),
);

if (options.computeSha1) {
const {capabilities} = await cmd('list-capabilities');
const {capabilities} = await cmd<WatchmanListCapabilitiesResponse>(
'list-capabilities',
);

if (capabilities.indexOf('field-content.sha1hex') !== -1) {
fields.push('content.sha1hex');
Expand All @@ -71,7 +100,10 @@ export = async function watchmanCrawl(
const watchmanRoots = new Map();
await Promise.all(
roots.map(async root => {
const response = await cmd('watch-project', root);
const response = await cmd<WatchmanWatchProjectResponse>(
'watch-project',
root,
);
const existing = watchmanRoots.get(response.watch);
// A root can only be filtered if it was never seen with a
// relative_path before.
Expand All @@ -96,7 +128,7 @@ export = async function watchmanCrawl(
}

async function queryWatchmanForDirs(rootProjectDirMappings: WatchmanRoots) {
const files = new Map();
const results = new Map<string, WatchmanQueryResponse>();
let isFresh = false;
await Promise.all(
Array.from(rootProjectDirMappings).map(
Expand All @@ -122,34 +154,49 @@ export = async function watchmanCrawl(
}

const relativeRoot = fastPath.relative(rootDir, root);
const query = clocks.has(relativeRoot)
? // Use the `since` generator if we have a clock available
{expression, fields, since: clocks.get(relativeRoot)}
: // Otherwise use the `glob` filter
{expression, fields, glob, glob_includedotfiles: true};

const response = await cmd('query', root, query);
const since = clocks.get(relativeRoot);
const query =
since !== undefined
? // Use the `since` generator if we have a clock available
{expression, fields, since}
: // Otherwise use the `glob` filter
{expression, fields, glob, glob_includedotfiles: true};

const response = await cmd<WatchmanQueryResponse>(
'query',
root,
query,
);

if ('warning' in response) {
console.warn('watchman warning: ', response.warning);
}

isFresh = isFresh || response.is_fresh_instance;
files.set(root, response);
// When a source-control query is used, we ignore the "is fresh"
// response from Watchman because it will be true despite the query
// being incremental.
const isSourceControlQuery =
typeof since !== 'string' &&
since?.scm?.['mergebase-with'] !== undefined;
if (!isSourceControlQuery) {
isFresh = isFresh || response.is_fresh_instance;
}

results.set(root, response);
},
),
);

return {
files,
isFresh,
results,
};
}

let files = data.files;
let removedFiles = new Map();
const changedFiles = new Map();
let watchmanFiles: Map<string, any>;
let results: Map<string, WatchmanQueryResponse>;
let isFresh = false;
try {
const watchmanRoots = await getWatchmanRoots(roots);
Expand All @@ -163,7 +210,7 @@ export = async function watchmanCrawl(
isFresh = true;
}

watchmanFiles = watchmanFileResults.files;
results = watchmanFileResults.results;
} finally {
client.end();
}
Expand All @@ -172,11 +219,16 @@ export = async function watchmanCrawl(
throw clientError;
}

// TODO: remove non-null
for (const [watchRoot, response] of watchmanFiles!) {
for (const [watchRoot, response] of results) {
const fsRoot = normalizePathSep(watchRoot);
const relativeFsRoot = fastPath.relative(rootDir, fsRoot);
clocks.set(relativeFsRoot, response.clock);
clocks.set(
relativeFsRoot,
// Ensure we persist only the local clock.
typeof response.clock === 'string'
? response.clock
: response.clock.clock,
);

for (const fileData of response.files) {
const filePath = fsRoot + path.sep + normalizePathSep(fileData.name);
Expand Down Expand Up @@ -209,7 +261,7 @@ export = async function watchmanCrawl(

let sha1hex = fileData['content.sha1hex'];
if (typeof sha1hex !== 'string' || sha1hex.length !== 40) {
sha1hex = null;
sha1hex = undefined;
}

let nextData: FileMetaData;
Expand All @@ -231,7 +283,7 @@ export = async function watchmanCrawl(
];
} else {
// See ../constants.ts
nextData = ['', mtime, size, 0, '', sha1hex];
nextData = ['', mtime, size, 0, '', sha1hex ?? null];
}

files.set(relativeFilePath, nextData);
Expand Down
3 changes: 2 additions & 1 deletion packages/jest-haste-map/src/types.ts
Expand Up @@ -55,7 +55,8 @@ export type FileMetaData = [

export type MockData = Map<string, Config.Path>;
export type ModuleMapData = Map<string, ModuleMapItem>;
export type WatchmanClocks = Map<Config.Path, string>;
export type WatchmanClockSpec = string | {scm: {'mergebase-with': string}};
export type WatchmanClocks = Map<Config.Path, WatchmanClockSpec>;
export type HasteRegExp = RegExp | ((str: string) => boolean);

export type DuplicatesSet = Map<string, /* type */ number>;
Expand Down