@tapjs/run
Version:
Command-line interface for the node-tap runner
178 lines • 6.55 kB
JavaScript
import { ProcessInfo } from '@tapjs/processinfo';
import { glob, Glob } from 'glob';
import { resolve } from 'node:path';
import { mainCommand, values } from './main-config.js';
import { readSave } from './save-list.js';
const alwaysExcludeNames = [
'.tap',
'.nyc_output',
'.git',
'.hg',
'node_modules',
'tap-snapshots',
];
const alwaysExcludePattern = `**/@(${alwaysExcludeNames.join('|')})/**`;
const defaultInclude = '**/{' +
'@(test?(s)|__test?(s)__)/**/*,' +
'*.@(test?(s)|spec),' +
'test?(s)' +
'}.__EXTENSIONS__';
const dirInclude = '**/*.__EXTENSIONS__';
// --save=<file>
// only run the files in the list, write failures back to it.
// If the file doesn't exist, run everything normally.
// Otherwise, don't delete coverage history.
// If they all pass, delete the file.
// --changed
// Figure out which files in the suite have changed since last run,
// and only run those. Do not delete coverage history ever.
export const list = async (args, config, noPruneUnchanged = false) => {
const saveList = new Set(await readSave(config));
if (args.length === 0)
args = config.get('files') || [];
const ignore = [alwaysExcludePattern];
if (values.exclude)
ignore.push(...values.exclude);
const g = new Glob(values.include || config.expandInclude(defaultInclude), {
ignore,
cwd: config.projectRoot,
withFileTypes: true,
});
const { scurry } = g;
// resolve non-existent paths as globs in the actual cwd
// turn everything into a Path in our main scurry, except
// stdin designators, which are left as strings
// This way the expected behavior of "the thing glob says"
// will work the same way across platforms, *and* the tests
// are all resolved to absolute paths that can be run from
// the cwd context of the project root.
const entries = new Set((await Promise.all((await (!args.length ?
g.walk()
: Promise.all(args.map(async (a) => {
if (a === '-' || a === '/dev/stdin')
return a;
const st = await scurry.cwd.resolve(resolve(a)).lstat();
if (st)
return st;
// from process cwd, because it's a cli positional
return glob(a, { absolute: true });
})))).reduce((set, entry) => {
// stat the glob results a second time, even though we know
// that they exist, because we need their stat info later.
if (Array.isArray(entry)) {
set.push(...entry.map(e => scurry.cwd.resolve(e).lstat()));
}
else if (entry === '-' || entry === '/dev/stdin') {
set.push(entry);
}
else {
set.push(entry);
}
return set;
}, []))).filter(p => {
// enoents should already be filtered out by glob, but just in case
/* c8 ignore start */
if (!p)
return false;
/* c8 ignore stop */
return true;
}));
await expandDirectories(config, scurry, entries, g.ignore);
const before = config.get('before');
const after = config.get('after');
if (before) {
entries.delete(scurry.cwd.resolve(before));
}
if (after) {
entries.delete(scurry.cwd.resolve(after));
}
if (saveList.size) {
for (const p of entries) {
if (typeof p === 'string')
continue;
if (!saveList.has(p.relativePosix()))
entries.delete(p);
}
}
const foundEntries = entries.size !== 0;
if (config.get('changed') && foundEntries && !noPruneUnchanged) {
await pruneUnchanged(scurry, entries);
}
const files = [...entries]
.map(p => (typeof p === 'string' ? p : p.relativePosix()))
.sort(config.get('shuffle') ?
() => Math.random() - 0.5
: (a, b) => a.localeCompare(b, 'en'));
if (mainCommand === 'list') {
if (foundEntries) {
// don't report an error if we found something but it's just not new
if (files.length)
console.log(files.join('\n').trimEnd());
}
else {
console.error('No files found.');
process.exitCode = 1;
if (args.length === 1) {
const maybe = args[0]?.match(/^(plugin|config)s?$/);
if (maybe) {
console.error(`(Did you mean 'tap ${maybe[1]} list'?)`);
}
}
}
}
return files;
};
const expandDirectories = async (config, scurry, entries, ignore) => {
// for each one that's a directory, expand it with the files it contains
// then go back to the original dir when we're done expanding.
const originalCwd = scurry.cwd;
for (const entry of entries) {
if (typeof entry === 'string')
continue;
if (entry.isDirectory()) {
entries.delete(entry);
scurry.chdir(entry);
// if we match a dir, then pull in any runnable files from within it
for (const s of await glob(config.expandInclude(dirInclude), {
scurry,
ignore,
withFileTypes: true,
cwd: scurry.cwd.fullpath(),
})) {
entries.add(s);
}
}
}
scurry.chdir(originalCwd);
};
// delete all the entries in the set that do not reference files
// that have changed since the last run.
const pruneUnchanged = async (scurry, entries) => {
const dir = scurry.resolve('.tap/processinfo');
const db = new ProcessInfo({ dir });
await db.load();
// for each entry in the list, find it in the processinfo db's list
// of externalIDs with changed files. but, we only need to check the
// ones for which there is SOME externalID node in the processinfo,
// because otherwise that means it's never been run.
const ids = new Map();
for (const e of entries) {
if (typeof e === 'string')
continue;
const rp = e.relativePosix();
const pi = db.externalIDs.get(rp);
if (!pi) {
// if there's no processinfo entry for it, then it must be new
// so don't even check it.
continue;
}
ids.set(rp, e);
}
const changed = await db.externalIDsChanged(id => ids.has(id));
for (const [id, e] of ids.entries()) {
if (!changed.has(id)) {
entries.delete(e);
}
}
};
//# sourceMappingURL=list.js.map