import {Worker} from 'jest-worker';
import {cpus} from 'os';
import process from 'process';
import * as readline from 'readline';
import ts from 'typescript';
import yargs from 'yargs';
import {hideBin} from 'yargs/helpers';
import {FILTER_PATH, PROJECT_ROOT} from './constants';
import {TestFilter, getFixtures, readTestFilter} from './fixture-utils';
import {TestResult, TestResults, report, update} from './reporter';
import {
RunnerAction,
RunnerState,
makeWatchRunner,
watchSrc,
} from './runner-watch';
import * as runnerWorker from './runner-worker';
import {execSync} from 'child_process';
const WORKER_PATH = require.resolve('./runner-worker.js');
const NUM_WORKERS = cpus().length - 1;
readline.emitKeypressEvents(process.stdin);
type RunnerOptions = {
sync: boolean;
workerThreads: boolean;
watch: boolean;
filter: boolean;
update: boolean;
};
const opts: RunnerOptions = yargs
.boolean('sync')
.describe(
'sync',
'Run compiler in main thread (instead of using worker threads or subprocesses). Defaults to false.',
)
.default('sync', false)
.boolean('worker-threads')
.describe(
'worker-threads',
'Run compiler in worker threads (instead of subprocesses). Defaults to true.',
)
.default('worker-threads', true)
.boolean('watch')
.describe('watch', 'Run compiler in watch mode, re-running after changes')
.default('watch', false)
.boolean('update')
.describe('update', 'Update fixtures')
.default('update', false)
.boolean('filter')
.describe(
'filter',
'Only run fixtures which match the contents of testfilter.txt',
)
.default('filter', false)
.help('help')
.strict()
.parseSync(hideBin(process.argv));
async function runFixtures(
worker: Worker & typeof runnerWorker,
filter: TestFilter | null,
compilerVersion: number,
): Promise<TestResults> {
const fixtures = await getFixtures(filter);
const isOnlyFixture = filter !== null && fixtures.size === 1;
let entries: Array<[string, TestResult]>;
if (!opts.sync) {
const work: Array<Promise<[string, TestResult]>> = [];
for (const [fixtureName, fixture] of fixtures) {
work.push(
worker
.transformFixture(
fixture,
compilerVersion,
(filter?.debug ?? false) && isOnlyFixture,
true,
)
.then(result => [fixtureName, result]),
);
}
entries = await Promise.all(work);
} else {
entries = [];
for (const [fixtureName, fixture] of fixtures) {
let output = await runnerWorker.transformFixture(
fixture,
compilerVersion,
(filter?.debug ?? false) && isOnlyFixture,
true,
);
entries.push([fixtureName, output]);
}
}
return new Map(entries);
}
async function onChange(
worker: Worker & typeof runnerWorker,
state: RunnerState,
) {
const {compilerVersion, isCompilerBuildValid, mode, filter} = state;
if (isCompilerBuildValid) {
const start = performance.now();
console.log('\u001Bc');
const results = await runFixtures(
worker,
mode.filter ? filter : null,
compilerVersion,
);
const end = performance.now();
if (mode.action === RunnerAction.Update) {
update(results);
state.lastUpdate = end;
} else {
report(results);
}
console.log(`Completed in ${Math.floor(end - start)} ms`);
} else {
console.error(
`${mode}: Found errors in Forget source code, skipping test fixtures.`,
);
}
console.log(
'\n' +
(mode.filter
? `Current mode = FILTER, filter test fixtures by "${FILTER_PATH}".`
: 'Current mode = NORMAL, run all test fixtures.') +
'\nWaiting for input or file changes...\n' +
'u - update all fixtures\n' +
`f - toggle (turn ${mode.filter ? 'off' : 'on'}) filter mode\n` +
'q - quit\n' +
'[any] - rerun tests\n',
);
}
export async function main(opts: RunnerOptions): Promise<void> {
const worker: Worker & typeof runnerWorker = new Worker(WORKER_PATH, {
enableWorkerThreads: opts.workerThreads,
numWorkers: NUM_WORKERS,
}) as any;
worker.getStderr().pipe(process.stderr);
worker.getStdout().pipe(process.stdout);
if (opts.watch) {
makeWatchRunner(state => onChange(worker, state), opts.filter);
if (opts.filter) {
for (let i = 0; i < NUM_WORKERS - 1; i++) {
worker.transformFixture(
{
fixturePath: 'tmp',
snapshotPath: './tmp.expect.md',
inputPath: './tmp.js',
input: `
function Foo(props) {
return identity(props);
}
`,
snapshot: null,
},
0,
false,
false,
);
}
}
} else {
const tsWatch: ts.WatchOfConfigFile<ts.SemanticDiagnosticsBuilderProgram> =
watchSrc(
() => {},
async (isTypecheckSuccess: boolean) => {
let isSuccess = false;
if (!isTypecheckSuccess) {
console.error(
'Found typescript errors in Forget source code, skipping test fixtures.',
);
} else {
try {
execSync('yarn build', {cwd: PROJECT_ROOT});
console.log('Built compiler successfully with tsup');
const testFilter = opts.filter ? await readTestFilter() : null;
const results = await runFixtures(worker, testFilter, 0);
if (opts.update) {
update(results);
isSuccess = true;
} else {
isSuccess = report(results);
}
} catch (e) {
console.warn('Failed to build compiler with tsup:', e);
}
}
tsWatch?.close();
await worker.end();
process.exit(isSuccess ? 0 : 1);
},
);
}
}
main(opts).catch(error => console.error(error));