@syncerauk/test-rules 1.0.1 → 1.0.3

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.
Files changed (3) hide show
  1. package/README.md +17 -2
  2. package/package.json +1 -1
  3. package/test-rules.mjs +32 -12
package/README.md CHANGED
@@ -49,8 +49,10 @@ 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), `reads` (the reads register: `"comment"` or
53
- `{ "json": "<path>" }`). The defaults are `CONFIG_DEFAULTS` in the script.
52
+ one list of what a person reads), `reads` (the reads register: `"comment"`, or
53
+ `{ "json": "<path>" }` for the map under `"tests"` in that file, `"key": "<name>"` for another
54
+ key, `"key": null` when the file itself is the map). The defaults are `CONFIG_DEFAULTS` in the
55
+ script.
54
56
  - `listed.json`: the breaches the repo had on the day the check arrived. Written once by
55
57
  `test-rules --init`.
56
58
 
@@ -63,6 +65,19 @@ Both in the repo's `scripts/test-rules/`:
63
65
 
64
66
  A repo starts with `"fail": false`: breaches are printed and the exit is 0. One line turns it on.
65
67
 
68
+ ## Versions
69
+
70
+ - **1.0.3** — A JSON reads register no longer turns off the scan of the test's code. A source
71
+ file the test builds a path to from its own place, in a test that reads files, is a
72
+ `T4:source-text` breach unless the test declares it `// runs:`. A repo with a JSON register
73
+ (app, portal) can see breaches that are not on its list on its first run of 1.0.3.
74
+ - **1.0.2** — A path that starts with a dot-folder (`.github/workflows/gate.yml`) is a repo
75
+ path; only `./` and `../` are relative to the test. A JSON reads register may be the whole
76
+ file (`"key": null`); a register with no map at its key cannot run (exit 3) instead of reading
77
+ as empty. Both make the check see reads it missed, so a repo's first run on 1.0.2 can show
78
+ breaches that are not on its list.
79
+ - **1.0.1** — The first release through the publish workflow. Same file as 1.0.0.
80
+
66
81
  ## Releases
67
82
 
68
83
  The source is `scripts/test-rules.mjs` in Syncera-UK/syncera-orchestrator, where its tests run
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@syncerauk/test-rules",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
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
@@ -33,11 +33,13 @@
33
33
  // T4:source-text the test reads a source file as text: a read it declares (the repo's reads
34
34
  // register) that is a source file or a folder and is neither under a fixture
35
35
  // folder nor on the wording list; a `?raw` import or a raw import.meta.glob;
36
- // and, where the register is comment lines or absent, a source file whose
37
- // path the test builds from its own place (new URL(…, import.meta.url), or
38
- // join( / resolve( on import.meta.dirname, __dirname, ROOT or HERE) in a test
39
- // that reads the file system and declares it neither `// reads:` nor
40
- // `// runs:` (a file that is only run is declared `// runs:`)
36
+ // and, whatever the register's form, a source file whose path the test
37
+ // builds from its own place (new URL(…, import.meta.url), or join( /
38
+ // resolve( on import.meta.dirname, __dirname, ROOT or HERE) in a test that
39
+ // reads the file system and declares it neither `// reads:`, `// runs:` nor
40
+ // in its JSON register entry (a file that is only run is declared `// runs:`).
41
+ // 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).
41
43
  // T6:timer setTimeout( / setInterval( / sleep( / timers/promises with no fake timers in the file
42
44
  // T6:clock Date.now( / new Date() / performance.now( with no fake timers in the file
43
45
  // T6:outside homedir(), process.env.HOME, a relative path that climbs out of the repo,
@@ -69,7 +71,7 @@ export const CONFIG_DEFAULTS = Object.freeze({
69
71
  ignore: [], // test files the rules do not judge (a real-time tier 3 suite)
70
72
  fixtures: [], // folders a test may read: `a/fixtures/`
71
73
  wording: [], // the one list of what a person reads (a built page, a legal text): files, globs, folders
72
- reads: null, // the reads register: null, "comment" (`// reads:` lines) or { json: "<path>", key: "tests" }
74
+ reads: null, // the reads register: null, "comment" (`// reads:` lines) or { json: "<path>", key: "tests" } (key null: the file itself is the map)
73
75
  source_ext: ['mjs', 'cjs', 'js', 'jsx', 'ts', 'tsx', 'mts', 'cts', 'sql', 'css', 'scss', 'html', 'sh', 'yml', 'yaml', 'md', 'mdx'],
74
76
  max_lines: 1500,
75
77
  task_names: ['(?:^|[^a-z0-9])w-?\\d+', '(?:^|[^a-z0-9])(?:fs|ops|trk|ci|app|leg|mig|bank|push|fun)-\\d+', '(?:^|[^a-z0-9])s\\d+(?:[^a-z0-9]|$)'],
@@ -204,7 +206,8 @@ const firstIndex = (text, re) => { const m = re.exec(text); return m ? m.index :
204
206
 
205
207
  // ---------------------------------------------------------------------------------------------
206
208
  // The reads register. Comment form: `// reads: a b` and `// runs: a b` on comment lines of their
207
- // own (the orchestrator's declared reads). JSON form: { <key>: { "<test>": ["<pattern>", …] } }.
209
+ // own (the orchestrator's declared reads). JSON form: { <key>: { "<test>": ["<pattern>", …] } },
210
+ // or the file itself is that map (`"key": null`): registerOf, below the config.
208
211
  // ---------------------------------------------------------------------------------------------
209
212
  const DECLARED = /^[ \t]*\/\/[ \t]*(reads|runs):[ \t]*(\S.*)$/gm;
210
213
  export function declaredIn(text) {
@@ -290,8 +293,8 @@ function sourceTextLine({ path, text, code, bare, strings }, { config, register,
290
293
  if (/\?raw|as\s*:\s*['"]raw['"]/.test(call)) return lineOf(code, m.index);
291
294
  }
292
295
  // A source file whose path the test builds from its own place, in a test that reads the file
293
- // system, where no register says what the test reads: declared `// runs:` when it is only run.
294
- if (register && typeof register === 'object') return 0;
296
+ // system, that the test does not declare: `// runs:` when it is only run. A JSON register does
297
+ // not silence this: a register can miss a read, and a source file it names returned above.
295
298
  if (!FS_READ.test(code) || !tracked) return 0;
296
299
  const said = [...declared.reads, ...declared.runs].map((r) => r.pattern);
297
300
  for (const s of strings) {
@@ -299,8 +302,9 @@ function sourceTextLine({ path, text, code, bare, strings }, { config, register,
299
302
  if (/[\s*?$]/.test(v) || !isSource(v)) continue;
300
303
  const before = code.slice(code.lastIndexOf('\n', s.start) + 1, s.start - 1);
301
304
  if (IMPORT_BEFORE.test(before) || !BUILT_HERE.test(before)) continue;
305
+ // `./x` and `../x` are relative to the test; `.github/x` is a repo path like any other.
302
306
  const here = posix.normalize(posix.join(posix.dirname(path), v));
303
- const target = v.startsWith('.') || !tracked.has(v) ? here : v;
307
+ const target = v.startsWith('./') || v.startsWith('../') || !tracked.has(v) ? here : v;
304
308
  if (target === path || !tracked.has(target) || allowed(target)) continue;
305
309
  if (said.some((p) => globRe(p).test(target))) continue;
306
310
  return lineOf(code, s.start);
@@ -394,10 +398,26 @@ export function withDefaults(raw) {
394
398
  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`);
395
399
  }
396
400
  const r = config.reads;
397
- if (!(r === null || r === 'comment' || (r && typeof r === 'object' && typeof r.json === 'string'))) throw fail(EXIT.FAILED, `${CONFIG_FILE}: reads must be null, "comment" or { "json": "<path>" }`);
401
+ const json = r && typeof r === 'object' && !Array.isArray(r) && typeof r.json === 'string'
402
+ && Object.keys(r).every((k) => k === 'json' || k === 'key') && (r.key === undefined || r.key === null || typeof r.key === 'string');
403
+ if (!(r === null || r === 'comment' || json)) throw fail(EXIT.FAILED, `${CONFIG_FILE}: reads must be null, "comment" or { "json": "<path>", "key": "<name>" | null }`);
398
404
  return config;
399
405
  }
400
406
 
407
+ /**
408
+ * The reads register inside a parsed JSON file: the map at `reads.key` ("tests" when no key is
409
+ * named), or the whole file when the key is null or empty. No map there cannot run (exit 3): an
410
+ * empty register would read as "no test reads anything".
411
+ */
412
+ export function registerOf(file, reads) {
413
+ const key = reads.key === undefined ? 'tests' : reads.key;
414
+ const whole = key === null || key === '';
415
+ const map = whole ? file : file?.[key];
416
+ const isMap = map && typeof map === 'object' && !Array.isArray(map) && Object.values(map).every((v) => Array.isArray(v));
417
+ if (!isMap) throw fail(EXIT.FAILED, `${reads.json}: no ${whole ? 'top-level' : `"${key}"`} map of { "<test file>": ["<pattern>", …] }${whole ? '' : ' (name it with "key", or "key": null when the file itself is the map)'}`);
418
+ return map;
419
+ }
420
+
401
421
  // ---------------------------------------------------------------------------------------------
402
422
  // The command line: the only part that touches disk and git.
403
423
  // ---------------------------------------------------------------------------------------------
@@ -446,7 +466,7 @@ export function main(argv = [], { log = console.log, error = console.error } = {
446
466
  const tracked = new Set(present);
447
467
  const testPaths = present.filter((p) => !p.split('/').includes('node_modules') && matches(config.tests, p) && !matches(config.ignore, p)).sort();
448
468
  const files = testPaths.map((path) => ({ path, text: readFileSync(join(dir, path), 'utf8') }));
449
- const register = config.reads && typeof config.reads === 'object' ? (readJson(dir, config.reads.json)[config.reads.key ?? 'tests'] ?? {}) : null;
469
+ const register = config.reads && typeof config.reads === 'object' ? registerOf(readJson(dir, config.reads.json), config.reads) : null;
450
470
  const all = breachesOf({ files, config, register, tracked });
451
471
 
452
472
  if (opts.init) {