Skip to content

Commit

Permalink
feat: cache key + tests
Browse files Browse the repository at this point in the history
  • Loading branch information
huafu committed Aug 12, 2018
1 parent f2e1da2 commit bd55448
Show file tree
Hide file tree
Showing 24 changed files with 401 additions and 106 deletions.
1 change: 0 additions & 1 deletion .npmignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
# Tests
e2e
test

# Developement scripts
scripts
Expand Down
13 changes: 6 additions & 7 deletions e2e/jest.config.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
const base = require('../jest.config');

module.exports = {
transform: {
'\\.ts$': '<rootDir>/../dist/index.js',
},
testRegex: '/__tests__/.+\\.test\\.ts$',
collectCoverageFrom: ['<rootDir>/../src/**/*.ts'],
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'],
testEnvironment: 'node',
...base,
rootDir: '.',
testRegex: '/__tests__/.+\\.(test|spec)\\.ts$',
coverageDirectory: '<rootDir>/../coverage/e2e',
snapshotSerializers: ['<rootDir>/__serializers__/test-run-result.ts'],
};
12 changes: 9 additions & 3 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
module.exports = {
rootDir: './test',
rootDir: 'src',
transform: {
'\\.ts$': '<rootDir>/../dist/index.js',
},
testRegex: '/.+\\.spec\\.ts$',
collectCoverageFrom: ['<rootDir>/../src/**/*.ts'],
testRegex: '\\.(spec|test)\\.ts$',
coverageDirectory: '<rootDir>/../coverage/unit',
collectCoverageFrom: [
'<rootDir>/../src/**/*.ts',
'!<rootDir>/../src/**/*.spec.ts',
'!<rootDir>/../src/**/*.test.ts',
'!<rootDir>/../src/**/__*__/',
],
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'],
testEnvironment: 'node',
};
5 changes: 5 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"devDependencies": {
"@types/babel__core": "^7.0.1",
"@types/fs-extra": "5.0.4",
"@types/gist-package-json": "git+https://gist.github.com/5c1cc527fe6b5b7dba41fec7fe54bf6e.git",
"@types/jest": "^23.3.1",
"@types/node": "^10.5.7",
"closest-file-data": "^0.1.4",
Expand Down
34 changes: 30 additions & 4 deletions test/__helpers__/sources-mock.ts → src/__helpers__/fakers.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import { resolve } from 'path';
import { TsJestGlobalOptions } from '../types';
import { resolve, relative } from 'path';
import spyThese from './spy-these';
import realFs from 'fs';

export function filePathMock(relPath: string): string {
export function filePath(relPath: string): string {
return resolve(__dirname, '..', '..', relPath);
}

export function transpiledTsSourceMock() {
export const rootDir = filePath('');

export function transpiledTsSource() {
return `
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
Expand All @@ -23,7 +28,7 @@ describe('hello', function () {
`;
}

export function tsSourceMock() {
export function typescriptSource() {
return `
import upper from './upper';
import lower from './lower';
Expand All @@ -39,3 +44,24 @@ describe('hello', () => {
});
`;
}

export function tsJestConfig<T extends TsJestGlobalOptions>(
options?: TsJestGlobalOptions,
): T {
return { ...options } as any;
}

export function jestConfig<T extends jest.ProjectConfig>(
options?: jest.InitialOptions,
tsJestOptions?: TsJestGlobalOptions,
): T {
const res = {
globals: {},
moduleFileExtensions: ['ts', 'js'],
...options,
} as any;
if (tsJestOptions) {
res.globals['ts-jest'] = tsJestConfig(tsJestOptions);
}
return res;
}
29 changes: 29 additions & 0 deletions src/__helpers__/spy-these.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
export default function spyThese<T extends object, K extends keyof T>(
object: T,
implementations: { [key in K]: T[K] | any | undefined },
): { [key in K]: jest.SpyInstance<T[K]> } & { mockRestore: () => void } {
const keys = Object.keys(implementations) as K[];
const res = keys.reduce(
(map, key) => {
const actual = object[key] as any;
const spy = jest.spyOn(object, key as K);
if (implementations[key]) {
const impl = implementations[key] as (...args: any[]) => any;
if (impl.length && /\W\$super\W/.test(impl.toString())) {
spy.mockImplementation(function(this: T, ...args: any[]) {
return impl.call(this, () => actual.apply(this, args), ...args);
});
} else {
spy.mockImplementation(impl);
}
}
return map;
},
{} as any,
);
// utility to restore all
res.mockRestore = () => {
keys.forEach(key => res[key].mockRestore());
};
return res;
}
21 changes: 21 additions & 0 deletions src/__mocks__/ts-program.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import * as fakers from '../__helpers__/fakers';
import { TsJestConfig, TsJestProgram } from '../types';
import { ParsedCommandLine } from 'typescript';

// tslint:disable-next-line:variable-name
export let __tsConfig: any = {};

export default class TsProgramMock implements TsJestProgram {
get parsedConfig(): ParsedCommandLine {
return __tsConfig;
}

constructor(
public rootDir: string = fakers.filePath(''),
public tsJestConfig: TsJestConfig = fakers.tsJestConfig(),
) {}

transpileModule(_: string, source: string) {
return source;
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`hoisting should hoist jest.mock calls using babel 1`] = `
exports[`process hoisting should hoist jest.mock calls using babel 1`] = `
"
\\"use strict\\";
Expand Down
File renamed without changes.
92 changes: 92 additions & 0 deletions src/ts-jest-transformer.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import TsJestTransformer from './ts-jest-transformer';
import * as fakers from './__helpers__/fakers';
import * as babelCfg from './utils/babel-config';
import * as closesPkgJson from './utils/closest-package-json';
import * as TsJestProgram from './ts-program';

jest.mock('./ts-program');
jest.mock('./utils/closest-package-json');
jest.mock('./utils/backports');

const mocks = {
babelConfig: undefined as any,
set packageJson(val: any) {
(closesPkgJson as any).__default = val;
},
set tsConfig(val: any) {
(TsJestProgram as any).__tsConfig = val;
},
reset() {
this.babelConfig = undefined;
this.packageJson = { name: 'mock' };
this.tsConfig = {};
},
};
beforeAll(() => {
jest
.spyOn(babelCfg, 'loadDefault')
.mockImplementation(() => mocks.babelConfig);
});
afterEach(() => {
mocks.reset();
});

describe('process', () => {
describe('hoisting', () => {
const transformer = new TsJestTransformer();
it('should hoist jest.mock calls using babel', () => {
const config = fakers.jestConfig({}, { babelJest: true });
const result = transformer.process(
fakers.transpiledTsSource(),
fakers.filePath('path/to/file.ts'),
config,
) as jest.TransformedSource;
expect(result.code).toMatchSnapshot();
});
}); // hoisting
}); // process

describe('getCacheKey', () => {
const fakeSource = fakers.typescriptSource();
const fakeFilePath = fakers.filePath('file.ts');
const fakeJestConfig = JSON.stringify(
fakers.jestConfig({}, { babelJest: true }),
);

const call: typeof TsJestTransformer['prototype']['getCacheKey'] = (
// tslint:disable-next-line:trailing-comma
...args
) => new TsJestTransformer().getCacheKey(...args);
const defaultCall = () => call(fakeSource, fakeFilePath, fakeJestConfig);

it('should be a 28 chars string, different for each case', () => {
const allCacheKeys = [
defaultCall(),
call('const b = 2', fakeFilePath, fakeJestConfig),
call(fakeSource, fakers.filePath('other-file.ts'), fakeJestConfig),
call(fakeSource, fakeFilePath, '{"rootDir": "./sub"}'),
call(fakeSource, fakeFilePath, fakeJestConfig, { instrument: true }),
call(fakeSource, fakeFilePath, fakeJestConfig, { rootDir: '/child' }),
];

mocks.babelConfig = '{sourceMaps: true}';
allCacheKeys.push(defaultCall());
mocks.reset();

mocks.tsConfig = '{"files": []}';
allCacheKeys.push(defaultCall());
mocks.reset();

mocks.packageJson = '{"name": "dummy"}';
allCacheKeys.push(defaultCall());
mocks.reset();

// uniq array should equal original
expect(
allCacheKeys.filter((k, i) => allCacheKeys.indexOf(k) === i),
).toEqual(allCacheKeys);
allCacheKeys.forEach(cacheKey => {
expect(cacheKey).toHaveLength(28);
});
}); // all different and 28 chars
}); // getCacheKey
70 changes: 62 additions & 8 deletions src/ts-jest-transformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,23 @@ import jestRootDir from './utils/jest-root-dir';
import { sep, resolve } from 'path';
import parseJsonUnsafe from './utils/parse-json-unsafe';
import * as babelCfg from './utils/babel-config';

// TODO: could be used if we need to handle the cache key ourself
// const normalizeJestConfig = (jestConfig: jest.ProjectConfig): jest.ProjectConfig => {
// const config = { ...jestConfig, rootDir: rootDirFor(jestConfig) };
// delete config.cacheDirectory;
// delete config.name;
// return config;
// };
import closestPatckageJson from './utils/closest-package-json';
import sha1 from './utils/sha1';

export default class TsJestTransformer implements jest.Transformer {
@Memoize(jestRootDir)
sanitizedJestConfigFor<T extends jest.ProjectConfig | jest.InitialOptions>(
jestConfig: T,
): T {
const config = {
...(jestConfig as object),
rootDir: jestRootDir(jestConfig),
} as T;
delete config.cacheDirectory;
delete config.name;
return config;
}

@Memoize(jestRootDir)
babelJestFor(jestConfig: jest.ProjectConfig): jest.Transformer {
// babel-jest is shipped with jest, no need to use the importer
Expand Down Expand Up @@ -99,6 +106,53 @@ export default class TsJestTransformer implements jest.Transformer {
return result;
}

// we can cache as for same instance the cache key won't change as soon as the path/content pair
// doesn't change
// TODO: find out if jest is already using this cache strategy and remove it if so
@Memoize((data: string, path: string) => `${path}::${data}`)
getCacheKey(
fileContent: string,
filePath: string,
jestConfigStr: string,
{
instrument = false,
rootDir,
}: { instrument?: boolean; rootDir?: string } = {},
): string {
const CHAR0 = '\0';
// will be used as the hashing data source
const hashData: string[] = [];
const hashUpdate = (data: string) => hashData.push(data, CHAR0);

// add file path and its content
hashUpdate(filePath);
hashUpdate(fileContent);

// saniize and normalize jest config
const jestConfig: jest.ProjectConfig = JSON.parse(jestConfigStr);
jestConfig.rootDir = rootDir = jestRootDir({
rootDir: rootDir || jestConfig.rootDir,
});
const sanitizedJestConfig: jest.ProjectConfig = this.sanitizedJestConfigFor(
jestConfig,
);
// add jest config
hashUpdate(JSON.stringify(sanitizedJestConfig));
// add project's package.json
const projectPkg = closestPatckageJson(rootDir, true);
hashUpdate(projectPkg);
// add babel config if using babel jest
const babelConfig = this.babelConfigFor(jestConfig) || {};
hashUpdate(JSON.stringify(babelConfig));
// add tsconfig
const tsConfig = this.programFor(sanitizedJestConfig).parsedConfig;
hashUpdate(JSON.stringify(tsConfig));
// add instrument, even if we don't use it since `canInstrument` is false
hashUpdate(`instrument:${instrument ? 'on' : 'off'}`);

return sha1(...hashData);
}

// TODO: use jest-config package to try to get current config and see if we are going to use babel jest or not
// in which case we'd return `true` there:
// get canInstrument() {}
Expand Down
8 changes: 4 additions & 4 deletions test/ts-program.spec.ts → src/ts-program.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import TsProgram from '../src/ts-program';
import TsProgram from './ts-program';
import { resolve } from 'path';
import { tsSourceMock, filePathMock } from './__helpers__/sources-mock';
import * as fakers from './__helpers__/fakers';

const path = filePathMock('path/to/file.ts');
const content = tsSourceMock();
const path = fakers.filePath('path/to/file.ts');
const content = fakers.typescriptSource();

describe('hoisting', () => {
describe('without babel', () => {
Expand Down
Loading

0 comments on commit bd55448

Please sign in to comment.