Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions build/tasks/verify/verify-unit.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/**
* @file Verify the build task helpers behave as their callers assume.
* @author The OpenINF Authors & Friends
* @license MIT OR Apache-2.0 OR BlueOak-1.0.0
* @module {type ES6Module} build/tasks/verify/verify-unit
*/

import { exec, glob } from '@openinf/portal/build/utils';

const testFiles = await glob(['**/*.test.mts', '!_site/', '!node_modules/']);

// `node --test` handed a pattern that matches nothing exits 0, so a task that
// only forwarded the pattern would report success having run no tests. The
// count is the guard against that -- and against `glob` itself finding
// nothing, which is among the failures these very tests exist to catch.
if (testFiles.length === 0) {
console.error('No test files matched `**/*.test.mts`.');
process.exitCode = 1;
} else {
let exitCode = 0;
const scripts = [`node --test ${testFiles.join(' ')}`];

for (const element of scripts) {
exitCode = await exec(element);

if (exitCode !== 0) process.exitCode = exitCode;
}
}
115 changes: 115 additions & 0 deletions build/utils.test.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/**
* @file Tests for the common build task utilities.
* @author The OpenINF Authors & Friends
* @license MIT OR Apache-2.0 OR BlueOak-1.0.0
* @module {type ES6Module} build/utils.test
*/

import { deepStrictEqual, ok } from 'node:assert/strict';
import { mkdir, mkdtemp, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { dirname, join as pathJoin } from 'node:path';
import { after, before, describe, test } from 'node:test';
import { glob } from '@openinf/portal/build/utils';

// Every pattern a build task writes is relative to the directory the task
// runs in, so the fixture has to become that directory.
const cwd = process.cwd();

/** Files laid out to cover what the tasks actually ask of `glob`. */
const FIXTURE = [
'a.md',
'.hidden.md', // a dot file beside ordinary ones
'sub/b.md',
'sub/.hidden-too.md', // a dot file under a directory named outright
'sub/nested/c.md',
'.dotdir/d.md', // a dot directory to descend through
'.dotdir/deep/e.md',
'skipped/f.md',
'skipped/.g.md', // a dot file inside an excluded directory
'.git/h.md', // git's own directory, never any task's business
'a.txt', // a different extension, to prove patterns discriminate
];

const sorted = (paths: string[]) => [...paths].sort();

describe('glob', () => {
before(async () => {
const root = await mkdtemp(pathJoin(tmpdir(), 'openinf-glob-'));

for (const path of FIXTURE) {
const full = pathJoin(root, path);

await mkdir(dirname(full), { recursive: true });
await writeFile(full, '');
}

process.chdir(root);
});

after(() => {
process.chdir(cwd);
});

test('returns paths relative to the working directory', async () => {
deepStrictEqual(await glob('a.md'), ['a.md']);
});

test('takes a lone pattern as well as a list', async () => {
deepStrictEqual(await glob(['a.md']), await glob('a.md'));
});

test('discriminates by extension', async () => {
deepStrictEqual(await glob('*.txt'), ['a.txt']);
});

test('excludes what a `!` pattern names', async () => {
const files = await glob(['**/*.md', '!skipped/']);

ok(!files.some((file) => file.startsWith('skipped/')));
ok(files.includes('sub/b.md'));
});

test('a trailing slash covers a whole subtree, not one entry', async () => {
// `sub/` on its own matches the directory and nothing in it, which is
// never what naming a directory is meant to mean. Dot files included:
// the pattern this expands to has no basename for the dot alternative to
// attach to, so they went missing until it was given one of its own.
deepStrictEqual(sorted(await glob('sub/')), [
'sub/.hidden-too.md',
'sub/b.md',
'sub/nested/c.md',
]);
});

test('never returns a directory', async () => {
// Callers paste the result into shell commands, where a directory
// argument makes the tool recurse and quietly undo the exclusions.
const files = await glob(['**/*', '!.git/']);

ok(!files.includes('sub'));
ok(!files.includes('.dotdir'));
ok(files.includes('sub/b.md'));
});

test('matches a dot file that a bare wildcard would skip', async () => {
ok((await glob('**/*.md')).includes('.hidden.md'));
});

test('descends into a dot directory', async () => {
const files = await glob('**/*.md');

ok(files.includes('.dotdir/d.md'));
ok(files.includes('.dotdir/deep/e.md'));
});

test('leaves .git alone without being asked', async () => {
ok(!(await glob('**/*.md')).some((file) => file.startsWith('.git/')));
});

test('prunes dot files inside an excluded directory', async () => {
// The exclusion is written without regard for dot entries, so pruning
// has to cover them or matching dot names would reopen what it closed.
ok(!(await glob(['**/*.md', '!skipped/'])).includes('skipped/.g.md'));
});
});
1 change: 1 addition & 0 deletions package-scripts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ scripts:
svg: node build/tasks/verify/verify-svg.mts
toml: node build/tasks/verify/verify-toml.mts
ts: node build/tasks/verify/verify-ts.mts
unit: node build/tasks/verify/verify-unit.mts
validForEC: node build/tasks/verify/verify-valid-for-ec.mts
yaml: node build/tasks/verify/verify-yaml.mts
format:
Expand Down