@syncerauk/test-rules 1.0.4 → 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 +9 -1
- package/package.json +1 -1
- package/test-rules.mjs +29 -5
package/README.md
CHANGED
|
@@ -56,7 +56,8 @@ Both in the repo's `scripts/test-rules/`:
|
|
|
56
56
|
key, `"key": null` when the file itself is the map). The defaults are `CONFIG_DEFAULTS` in the
|
|
57
57
|
script.
|
|
58
58
|
- `listed.json`: the breaches the repo had on the day the check arrived. Written once by
|
|
59
|
-
`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.
|
|
60
61
|
|
|
61
62
|
## The list only shrinks
|
|
62
63
|
|
|
@@ -64,11 +65,18 @@ Both in the repo's `scripts/test-rules/`:
|
|
|
64
65
|
- An entry whose breach is gone is stale and fails until its line is deleted.
|
|
65
66
|
- Against a base, an entry the base's list does not hold fails. The base is `--base <ref>`,
|
|
66
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.
|
|
67
71
|
|
|
68
72
|
A repo starts with `"fail": false`: breaches are printed and the exit is 0. One line turns it on.
|
|
69
73
|
|
|
70
74
|
## Versions
|
|
71
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.
|
|
72
80
|
- **1.0.4** — `style_contract`: a repo may name one test that reads its stylesheets (`.css`,
|
|
73
81
|
`.scss`), the way wording is read once from the wording list. Those files are not a
|
|
74
82
|
`T4:source-text` breach in that test, and only in that test. Nothing changes for a repo that
|
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:
|
|
@@ -349,9 +355,12 @@ export function breachesOf({ files, config, register = null, tracked = null }) {
|
|
|
349
355
|
* The verdict over the breaches and the list. `listed`: the head's entries ({ file: [rule] }).
|
|
350
356
|
* `base`: null (no base read), or { listed: the base's entries or null when the base has no
|
|
351
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.
|
|
352
361
|
* → { breaches (off the list, plus T1:hub and list:grew), listed (count on the list), stale }.
|
|
353
362
|
*/
|
|
354
|
-
export function verdict({ all, listed = {}, base = null, lineCounts = {} }) {
|
|
363
|
+
export function verdict({ all, listed = {}, base = null, lineCounts = {}, moved = {}, present = null }) {
|
|
355
364
|
const on = (file, rule) => (listed[file] ?? []).includes(rule) && !NEVER_LISTED.includes(rule);
|
|
356
365
|
const breaches = all.filter((b) => !on(b.file, b.rule));
|
|
357
366
|
const hits = all.filter((b) => on(b.file, b.rule));
|
|
@@ -360,10 +369,14 @@ export function verdict({ all, listed = {}, base = null, lineCounts = {} }) {
|
|
|
360
369
|
for (const file of Object.keys(listed).sort()) {
|
|
361
370
|
for (const rule of [...listed[file]].sort()) if (!found.has(`${file}\n${rule}`)) stale.push({ file, rule });
|
|
362
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
|
+
};
|
|
363
376
|
if (base) {
|
|
364
377
|
for (const file of Object.keys(listed).sort()) {
|
|
365
378
|
for (const rule of [...listed[file]].sort()) {
|
|
366
|
-
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']}` });
|
|
367
380
|
}
|
|
368
381
|
const was = base.lines?.[file];
|
|
369
382
|
if (listed[file].includes('R7:lines') && Number.isFinite(was) && lineCounts[file] > was) {
|
|
@@ -462,6 +475,15 @@ export function entriesOf(obj, where) {
|
|
|
462
475
|
if (!ok) throw fail(EXIT.FAILED, `${where}: must be { "v": ${VERSION}, "entries": { "<test file>": ["<rule id>", …] } }`);
|
|
463
476
|
return e;
|
|
464
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
|
+
}
|
|
465
487
|
|
|
466
488
|
export function main(argv = [], { log = console.log, error = console.error } = {}) {
|
|
467
489
|
try {
|
|
@@ -486,7 +508,9 @@ export function main(argv = [], { log = console.log, error = console.error } = {
|
|
|
486
508
|
return EXIT.CLEAN;
|
|
487
509
|
}
|
|
488
510
|
|
|
489
|
-
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) : {};
|
|
490
514
|
let base = null;
|
|
491
515
|
let baseNote = 'no base';
|
|
492
516
|
if (!opts.noBase) {
|
|
@@ -512,7 +536,7 @@ export function main(argv = [], { log = console.log, error = console.error } = {
|
|
|
512
536
|
} else baseNote = `no base (${ref} does not resolve)`;
|
|
513
537
|
}
|
|
514
538
|
const lineCounts = Object.fromEntries(files.map((f) => [f.path, countLines(f.text)]));
|
|
515
|
-
const v = verdict({ all, listed, base, lineCounts });
|
|
539
|
+
const v = verdict({ all, listed, base, lineCounts, moved, present: new Set(testPaths) });
|
|
516
540
|
|
|
517
541
|
if (opts.list) {
|
|
518
542
|
log(JSON.stringify({ v: VERSION, breaches: v.breaches, listed: v.listed, stale: v.stale }));
|