@syncerauk/test-rules 1.0.3 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -2
- package/package.json +1 -1
- package/test-rules.mjs +40 -7
package/README.md
CHANGED
|
@@ -49,12 +49,15 @@ Both in the repo's `scripts/test-rules/`:
|
|
|
49
49
|
|
|
50
50
|
- `config.json`: what differs between repos. `fail` (false is report only), `tests` and
|
|
51
51
|
`ignore` (which files are test files), `fixtures` (folders a test may read), `wording` (the
|
|
52
|
-
one list of what a person reads), `
|
|
52
|
+
one list of what a person reads), `style_contract` (`{ "<test file>": ["<.css or .scss path or
|
|
53
|
+
glob>"] }`: the one test that may read those stylesheets; style rules are checked there, once),
|
|
54
|
+
`reads` (the reads register: `"comment"`, or
|
|
53
55
|
`{ "json": "<path>" }` for the map under `"tests"` in that file, `"key": "<name>"` for another
|
|
54
56
|
key, `"key": null` when the file itself is the map). The defaults are `CONFIG_DEFAULTS` in the
|
|
55
57
|
script.
|
|
56
58
|
- `listed.json`: the breaches the repo had on the day the check arrived. Written once by
|
|
57
|
-
`test-rules --init`.
|
|
59
|
+
`test-rules --init`. An optional `moved` map, `{ "<new test file>": "<old test file>" }`,
|
|
60
|
+
says where a split or a rename took a listed file.
|
|
58
61
|
|
|
59
62
|
## The list only shrinks
|
|
60
63
|
|
|
@@ -62,11 +65,22 @@ Both in the repo's `scripts/test-rules/`:
|
|
|
62
65
|
- An entry whose breach is gone is stale and fails until its line is deleted.
|
|
63
66
|
- Against a base, an entry the base's list does not hold fails. The base is `--base <ref>`,
|
|
64
67
|
else `origin/main` when it resolves.
|
|
68
|
+
- A split or a rename carries its listed breaches: the new file's entry passes when `moved`
|
|
69
|
+
names the old file, the base listed the same rule on it, and the old file is no longer a test
|
|
70
|
+
file. A `moved` line is never stale and never a breach; delete it whenever you like.
|
|
65
71
|
|
|
66
72
|
A repo starts with `"fail": false`: breaches are printed and the exit is 0. One line turns it on.
|
|
67
73
|
|
|
68
74
|
## Versions
|
|
69
75
|
|
|
76
|
+
- **1.0.5** — A split or rename carries its listed breaches: `listed.json` may hold
|
|
77
|
+
`"moved": { "<new test file>": "<old test file>" }`, and a new file's entry is not
|
|
78
|
+
`list:grew` when the base listed the same rule on the old file and the old file is gone.
|
|
79
|
+
Nothing changes for a repo that does not set it.
|
|
80
|
+
- **1.0.4** — `style_contract`: a repo may name one test that reads its stylesheets (`.css`,
|
|
81
|
+
`.scss`), the way wording is read once from the wording list. Those files are not a
|
|
82
|
+
`T4:source-text` breach in that test, and only in that test. Nothing changes for a repo that
|
|
83
|
+
does not set it.
|
|
70
84
|
- **1.0.3** — A JSON reads register no longer turns off the scan of the test's code. A source
|
|
71
85
|
file the test builds a path to from its own place, in a test that reads files, is a
|
|
72
86
|
`T4:source-text` breach unless the test declares it `// runs:`. A repo with a JSON register
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@syncerauk/test-rules",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
4
|
"description": "The static check of Syncera's test-writing rules (T1, T4, T6 and the line limit of R7). One file, no dependency. Published to npm by the orchestrator's publish-test-rules workflow; each repo pins an exact version.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|
package/test-rules.mjs
CHANGED
|
@@ -16,7 +16,9 @@
|
|
|
16
16
|
//
|
|
17
17
|
// Per repo, two files:
|
|
18
18
|
// scripts/test-rules/config.json what differs between repos (CONFIG_DEFAULTS below)
|
|
19
|
-
// scripts/test-rules/listed.json { v: 1, entries: { "<test file>": ["<rule id>", …] }
|
|
19
|
+
// scripts/test-rules/listed.json { v: 1, entries: { "<test file>": ["<rule id>", …] },
|
|
20
|
+
// moved: { "<new test file>": "<old test file>" } }
|
|
21
|
+
// `moved` is optional: it says where a split or a rename took a listed file (1.0.5).
|
|
20
22
|
//
|
|
21
23
|
// The list only shrinks. A breach on the list passes; a breach off the list fails; an entry
|
|
22
24
|
// whose breach is gone is `stale` and fails until its line is deleted; with a base, an entry the
|
|
@@ -24,6 +26,10 @@
|
|
|
24
26
|
// lines than at the base fails (`T1:hub`). The base is `--base <ref>`, else origin/main when it
|
|
25
27
|
// resolves (the merge base with HEAD is what is read); `--no-base` skips those two checks. The
|
|
26
28
|
// base having no list at all is the one change that may add it (`--init` writes it once).
|
|
29
|
+
// A split or a rename carries its listed breaches: an entry the base does not hold is not
|
|
30
|
+
// `list:grew` when `moved` names its old file, the base listed that same rule on the old file,
|
|
31
|
+
// and the old file is no longer a test file. A `moved` line is never stale and never a breach;
|
|
32
|
+
// it may be deleted at any time.
|
|
27
33
|
// `"fail": false` in the config is report only: everything is printed, the exit is 0.
|
|
28
34
|
//
|
|
29
35
|
// The rules (RULES below) and how each is seen:
|
|
@@ -39,7 +45,8 @@
|
|
|
39
45
|
// reads the file system and declares it neither `// reads:`, `// runs:` nor
|
|
40
46
|
// in its JSON register entry (a file that is only run is declared `// runs:`).
|
|
41
47
|
// A JSON register is what a test says it reads; it does not stop the code
|
|
42
|
-
// being read for what it does read (1.0.3).
|
|
48
|
+
// being read for what it does read (1.0.3). A stylesheet named for a test in
|
|
49
|
+
// `style_contract` is not a breach in that test, and only in that test (1.0.4).
|
|
43
50
|
// T6:timer setTimeout( / setInterval( / sleep( / timers/promises with no fake timers in the file
|
|
44
51
|
// T6:clock Date.now( / new Date() / performance.now( with no fake timers in the file
|
|
45
52
|
// T6:outside homedir(), process.env.HOME, a relative path that climbs out of the repo,
|
|
@@ -71,6 +78,7 @@ export const CONFIG_DEFAULTS = Object.freeze({
|
|
|
71
78
|
ignore: [], // test files the rules do not judge (a real-time tier 3 suite)
|
|
72
79
|
fixtures: [], // folders a test may read: `a/fixtures/`
|
|
73
80
|
wording: [], // the one list of what a person reads (a built page, a legal text): files, globs, folders
|
|
81
|
+
style_contract: {}, // { "<test file>": ["<.css/.scss path or glob>", …] }: the one test that may read those stylesheets
|
|
74
82
|
reads: null, // the reads register: null, "comment" (`// reads:` lines) or { json: "<path>", key: "tests" } (key null: the file itself is the map)
|
|
75
83
|
source_ext: ['mjs', 'cjs', 'js', 'jsx', 'ts', 'tsx', 'mts', 'cts', 'sql', 'css', 'scss', 'html', 'sh', 'yml', 'yaml', 'md', 'mdx'],
|
|
76
84
|
max_lines: 1500,
|
|
@@ -273,7 +281,9 @@ function envLine({ code, bare }, allowed) {
|
|
|
273
281
|
/** T4: the line of the first read of a source file as text, or 0. */
|
|
274
282
|
function sourceTextLine({ path, text, code, bare, strings }, { config, register, tracked }) {
|
|
275
283
|
const isSource = (p) => config.source_ext.includes(String(p).split('.').pop().toLowerCase());
|
|
276
|
-
|
|
284
|
+
// A style contract is the one test that may read the stylesheets it names: style rules are
|
|
285
|
+
// checked once, there, the way wording is checked once from the wording list.
|
|
286
|
+
const allowed = (p) => under(config.fixtures, p) || under(config.wording, p) || under(config.style_contract?.[path], p);
|
|
277
287
|
const isFolderOrGlob = (p) => (/[*?]/.test(p) ? !/\.[A-Za-z0-9]+$/.test(p) || isSource(p) : String(p).endsWith('/'));
|
|
278
288
|
const offends = (p) => !allowed(p) && (isFolderOrGlob(p) || isSource(p));
|
|
279
289
|
|
|
@@ -345,9 +355,12 @@ export function breachesOf({ files, config, register = null, tracked = null }) {
|
|
|
345
355
|
* The verdict over the breaches and the list. `listed`: the head's entries ({ file: [rule] }).
|
|
346
356
|
* `base`: null (no base read), or { listed: the base's entries or null when the base has no
|
|
347
357
|
* list, lines: { file: its line count at the base } }.
|
|
358
|
+
* `moved`: { new file: old file } from the head's list. `present`: a Set of the head's test
|
|
359
|
+
* files, or null (then nothing is carried). An entry the base does not hold is carried, not
|
|
360
|
+
* `list:grew`, when its old file is gone and the base listed the same rule on it.
|
|
348
361
|
* → { breaches (off the list, plus T1:hub and list:grew), listed (count on the list), stale }.
|
|
349
362
|
*/
|
|
350
|
-
export function verdict({ all, listed = {}, base = null, lineCounts = {} }) {
|
|
363
|
+
export function verdict({ all, listed = {}, base = null, lineCounts = {}, moved = {}, present = null }) {
|
|
351
364
|
const on = (file, rule) => (listed[file] ?? []).includes(rule) && !NEVER_LISTED.includes(rule);
|
|
352
365
|
const breaches = all.filter((b) => !on(b.file, b.rule));
|
|
353
366
|
const hits = all.filter((b) => on(b.file, b.rule));
|
|
@@ -356,10 +369,14 @@ export function verdict({ all, listed = {}, base = null, lineCounts = {} }) {
|
|
|
356
369
|
for (const file of Object.keys(listed).sort()) {
|
|
357
370
|
for (const rule of [...listed[file]].sort()) if (!found.has(`${file}\n${rule}`)) stale.push({ file, rule });
|
|
358
371
|
}
|
|
372
|
+
const carried = (file, rule) => {
|
|
373
|
+
const old = Object.hasOwn(moved, file) ? moved[file] : null;
|
|
374
|
+
return typeof old === 'string' && present !== null && !present.has(old) && (base.listed?.[old] ?? []).includes(rule);
|
|
375
|
+
};
|
|
359
376
|
if (base) {
|
|
360
377
|
for (const file of Object.keys(listed).sort()) {
|
|
361
378
|
for (const rule of [...listed[file]].sort()) {
|
|
362
|
-
if (base.listed && !(base.listed[file] ?? []).includes(rule)) breaches.push({ file, rule: 'list:grew', line: 1, say: `${rule}: ${RULES['list:grew']}` });
|
|
379
|
+
if (base.listed && !(base.listed[file] ?? []).includes(rule) && !carried(file, rule)) breaches.push({ file, rule: 'list:grew', line: 1, say: `${rule}: ${RULES['list:grew']}` });
|
|
363
380
|
}
|
|
364
381
|
const was = base.lines?.[file];
|
|
365
382
|
if (listed[file].includes('R7:lines') && Number.isFinite(was) && lineCounts[file] > was) {
|
|
@@ -397,6 +414,11 @@ export function withDefaults(raw) {
|
|
|
397
414
|
for (const k of ['tests', 'ignore', 'fixtures', 'wording', 'source_ext', 'task_names', 'fake_timers', 'env_allowed']) {
|
|
398
415
|
if (!Array.isArray(config[k]) || config[k].some((x) => typeof x !== 'string')) throw fail(EXIT.FAILED, `${CONFIG_FILE}: ${k} must be a list of strings`);
|
|
399
416
|
}
|
|
417
|
+
const sc = config.style_contract;
|
|
418
|
+
const styleOnly = (g) => typeof g === 'string' && /\.s?css$/.test(g);
|
|
419
|
+
if (!sc || typeof sc !== 'object' || Array.isArray(sc) || Object.values(sc).some((v) => !Array.isArray(v) || !v.length || !v.every(styleOnly))) {
|
|
420
|
+
throw fail(EXIT.FAILED, `${CONFIG_FILE}: style_contract must be { "<test file>": ["<path or glob ending .css or .scss>", …] }`);
|
|
421
|
+
}
|
|
400
422
|
const r = config.reads;
|
|
401
423
|
const json = r && typeof r === 'object' && !Array.isArray(r) && typeof r.json === 'string'
|
|
402
424
|
&& Object.keys(r).every((k) => k === 'json' || k === 'key') && (r.key === undefined || r.key === null || typeof r.key === 'string');
|
|
@@ -453,6 +475,15 @@ export function entriesOf(obj, where) {
|
|
|
453
475
|
if (!ok) throw fail(EXIT.FAILED, `${where}: must be { "v": ${VERSION}, "entries": { "<test file>": ["<rule id>", …] } }`);
|
|
454
476
|
return e;
|
|
455
477
|
}
|
|
478
|
+
/** The optional `moved` map of a list: { "<new test file>": "<old test file>" }, or {} when absent. */
|
|
479
|
+
export function movedOf(obj, where) {
|
|
480
|
+
if (obj?.moved === undefined) return {};
|
|
481
|
+
const m = obj.moved;
|
|
482
|
+
const ok = m && typeof m === 'object' && !Array.isArray(m)
|
|
483
|
+
&& Object.entries(m).every(([to, from]) => typeof from === 'string' && from.length > 0 && from !== to);
|
|
484
|
+
if (!ok) throw fail(EXIT.FAILED, `${where}: "moved" must be { "<new test file>": "<old test file>", … }, each old file named and not the new one`);
|
|
485
|
+
return m;
|
|
486
|
+
}
|
|
456
487
|
|
|
457
488
|
export function main(argv = [], { log = console.log, error = console.error } = {}) {
|
|
458
489
|
try {
|
|
@@ -477,7 +508,9 @@ export function main(argv = [], { log = console.log, error = console.error } = {
|
|
|
477
508
|
return EXIT.CLEAN;
|
|
478
509
|
}
|
|
479
510
|
|
|
480
|
-
const
|
|
511
|
+
const listedJson = existsSync(join(dir, LISTED_FILE)) ? readJson(dir, LISTED_FILE) : null;
|
|
512
|
+
const listed = listedJson ? entriesOf(listedJson, LISTED_FILE) : {};
|
|
513
|
+
const moved = listedJson ? movedOf(listedJson, LISTED_FILE) : {};
|
|
481
514
|
let base = null;
|
|
482
515
|
let baseNote = 'no base';
|
|
483
516
|
if (!opts.noBase) {
|
|
@@ -503,7 +536,7 @@ export function main(argv = [], { log = console.log, error = console.error } = {
|
|
|
503
536
|
} else baseNote = `no base (${ref} does not resolve)`;
|
|
504
537
|
}
|
|
505
538
|
const lineCounts = Object.fromEntries(files.map((f) => [f.path, countLines(f.text)]));
|
|
506
|
-
const v = verdict({ all, listed, base, lineCounts });
|
|
539
|
+
const v = verdict({ all, listed, base, lineCounts, moved, present: new Set(testPaths) });
|
|
507
540
|
|
|
508
541
|
if (opts.list) {
|
|
509
542
|
log(JSON.stringify({ v: VERSION, breaches: v.breaches, listed: v.listed, stale: v.stale }));
|