cli: optimize repo test to filter out ununsed projects

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2024-10-26 11:59:38 +02:00
parent 7621b04cad
commit 6819f8cef2
4 changed files with 107 additions and 14 deletions
+7 -3
View File
@@ -326,7 +326,7 @@ async function getRootConfig() {
),
).then(_ => _.flat());
let configs = await Promise.all(
let projects = await Promise.all(
projectPaths.flat().map(async projectPath => {
const packagePath = path.resolve(projectPath, 'package.json');
if (!(await fs.pathExists(packagePath))) {
@@ -357,12 +357,16 @@ async function getRootConfig() {
const cache = global.__backstageCli_jestSuccessCache;
if (cache) {
configs = await cache.filterConfigs(configs, globalRootConfig);
projects = await cache.filterConfigs(projects, globalRootConfig);
}
const watchProjectFilter = global.__backstageCli_watchProjectFilter;
if (watchProjectFilter) {
projects = await watchProjectFilter.filter(projects);
}
return {
rootDir: paths.targetRoot,
projects: configs,
projects,
testResultsProcessor: cache
? require.resolve('./jestCacheResultProcessor.cjs')
: undefined,
+50 -9
View File
@@ -17,6 +17,7 @@
import os from 'os';
import crypto from 'node:crypto';
import yargs from 'yargs';
import { run as runJest, yargsOptions as jestYargsOptions } from 'jest-cli';
import { relative as relativePath } from 'path';
import { Command, OptionValues } from 'commander';
import { Lockfile, PackageGraph } from '@backstage/cli-node';
@@ -27,9 +28,10 @@ import { SuccessCache } from '../../lib/cache/SuccessCache';
type JestProject = {
displayName: string;
rootDir: string;
};
interface GlobalWithCache extends Global {
interface TestGlobal extends Global {
__backstageCli_jestSuccessCache?: {
filterConfigs(
projectConfigs: JestProject[],
@@ -48,6 +50,9 @@ interface GlobalWithCache extends Global {
}>;
}): Promise<void>;
};
__backstageCli_watchProjectFilter?: {
filter(projectConfigs: JestProject[]): Promise<JestProject[]>;
};
}
/**
@@ -138,6 +143,8 @@ function removeOptionArg(args: string[], option: string, size: number = 2) {
}
export async function command(opts: OptionValues, cmd: Command): Promise<void> {
const testGlobal = global as TestGlobal;
// all args are forwarded to jest
let parent = cmd;
while (parent.parent) {
@@ -148,6 +155,9 @@ export async function command(opts: OptionValues, cmd: Command): Promise<void> {
const hasFlags = createFlagFinder(args);
// Parse the args to ensure that no file filters are provided, in which case we refuse to run
const { _: parsedArgs } = await yargs(args).options(jestYargsOptions).argv;
// Only include our config if caller isn't passing their own config
if (!hasFlags('-c', '--config')) {
args.push('--config', paths.resolveOwn('config/jest.js'));
@@ -158,6 +168,7 @@ export async function command(opts: OptionValues, cmd: Command): Promise<void> {
}
// Run in watch mode unless in CI, coverage mode, or running all tests
let isSingleWatchMode = args.includes('--watch');
if (
!opts.since &&
!process.env.CI &&
@@ -168,12 +179,47 @@ export async function command(opts: OptionValues, cmd: Command): Promise<void> {
const isMercurialRepo = () => runCheck('hg', '--cwd', '.', 'root');
if ((await isGitRepo()) || (await isMercurialRepo())) {
isSingleWatchMode = true;
args.push('--watch');
} else {
args.push('--watchAll');
}
}
// Due to our monorepo Jest project setup watch mode can be quite slow as it
// will always scan all projects for matches. This is an optimization where if
// the only provides filter paths from the repo root as args, we filter the
// projects to only run tests for those.
//
// This does mean you're not able to edit the watch filters during the watch
// session to point outside of the selected packages, but we consider that a
// worthwhile tradeoff, and you can always avoid providing paths upfront.
if (isSingleWatchMode && parsedArgs.length > 0) {
testGlobal.__backstageCli_watchProjectFilter = {
async filter(projectConfigs) {
const selectedProjects = [];
const usedArgs = new Set(parsedArgs);
for (const project of projectConfigs) {
for (const arg of usedArgs) {
if (isChildPath(project.rootDir, String(arg))) {
selectedProjects.push(project);
usedArgs.delete(arg);
}
}
}
// If we didn't end up using all args in the filtering we need to bail
// and let Jest do the full filtering instead.
if (usedArgs.size > 0) {
return projectConfigs;
}
return selectedProjects;
},
};
}
// When running tests from the repo root in large repos you can easily hit the heap limit.
// This is because Jest workers leak a lot of memory, and the workaround is to limit worker memory.
// We set a default memory limit, but if an explicit one is supplied it will be used instead
@@ -250,17 +296,13 @@ export async function command(opts: OptionValues, cmd: Command): Promise<void> {
(process.stdout as any)._handle.setBlocking(true);
}
const jestCli = require('jest-cli');
// This code path is enabled by the --successCache flag, which is specific to
// the `repo test` command in the Backstage CLI.
if (opts.successCache) {
removeOptionArg(args, '--successCache', 1);
removeOptionArg(args, '--successCacheDir');
// Parse the args to ensure that no file filters are provided, in which case we refuse to run
const { _: parsedArgs } = await yargs(args).options(jestCli.yargsOptions)
.argv;
// Refuse to run if file filters are provided
if (parsedArgs.length > 0) {
throw new Error(
`The --successCache flag can not be combined with the following arguments: ${parsedArgs.join(
@@ -284,8 +326,7 @@ export async function command(opts: OptionValues, cmd: Command): Promise<void> {
// Set up a bridge with the @backstage/cli/config/jest configuration file. These methods
// are picked up by the config script itself, as well as the custom result processor.
const globalWithCache = global as GlobalWithCache;
globalWithCache.__backstageCli_jestSuccessCache = {
testGlobal.__backstageCli_jestSuccessCache = {
// This is called by `config/jest.js` after the project configs have been gathered
async filterConfigs(projectConfigs, globalRootConfig) {
const cacheEntries = await cache.read();
@@ -381,5 +422,5 @@ export async function command(opts: OptionValues, cmd: Command): Promise<void> {
};
}
await jestCli.run(args);
await runJest(args);
}