/** * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.dev/license */ import glob, { isDynamicPattern } from 'fast-glob'; import { PathLike, constants, promises as fs } from 'fs'; import { basename, dirname, extname, join, relative } from 'path'; /* Go through all patterns and find unique list of files */ export async function findTests( include: string[], exclude: string[], workspaceRoot: string, projectSourceRoot: string, ): Promise { const matchingTestsPromises = include.map((pattern) => findMatchingTests(pattern, exclude, workspaceRoot, projectSourceRoot), ); const files = await Promise.all(matchingTestsPromises); // Unique file names return [...new Set(files.flat())]; } const normalizePath = (path: string): string => path.replace(/\\/g, '/'); const removeLeadingSlash = (pattern: string): string => { if (pattern.charAt(0) === '/') { return pattern.substring(1); } return pattern; }; const removeRelativeRoot = (path: string, root: string): string => { if (path.startsWith(root)) { return path.substring(root.length); } return path; }; async function findMatchingTests( pattern: string, ignore: string[], workspaceRoot: string, projectSourceRoot: string, ): Promise { // normalize pattern, glob lib only accepts forward slashes let normalizedPattern = normalizePath(pattern); normalizedPattern = removeLeadingSlash(normalizedPattern); const relativeProjectRoot = normalizePath(relative(workspaceRoot, projectSourceRoot) + '/'); // remove relativeProjectRoot to support relative paths from root // such paths are easy to get when running scripts via IDEs normalizedPattern = removeRelativeRoot(normalizedPattern, relativeProjectRoot); // special logic when pattern does not look like a glob if (!isDynamicPattern(normalizedPattern)) { if (await isDirectory(join(projectSourceRoot, normalizedPattern))) { normalizedPattern = `${normalizedPattern}/**/*.spec.@(ts|tsx)`; } else { // see if matching spec file exists const fileExt = extname(normalizedPattern); // Replace extension to `.spec.ext`. Example: `src/app/app.component.ts`-> `src/app/app.component.spec.ts` const potentialSpec = join( projectSourceRoot, dirname(normalizedPattern), `${basename(normalizedPattern, fileExt)}.spec${fileExt}`, ); if (await exists(potentialSpec)) { return [potentialSpec]; } } } // normalize the patterns in the ignore list const normalizedIgnorePatternList = ignore.map((pattern: string) => removeRelativeRoot(removeLeadingSlash(normalizePath(pattern)), relativeProjectRoot), ); return glob(normalizedPattern, { cwd: projectSourceRoot, absolute: true, ignore: ['**/node_modules/**', ...normalizedIgnorePatternList], }); } async function isDirectory(path: PathLike): Promise { try { const stats = await fs.stat(path); return stats.isDirectory(); } catch { return false; } } async function exists(path: PathLike): Promise { try { await fs.access(path, constants.F_OK); return true; } catch { return false; } }