2201c5c416938253128eebcac00c0a240574a889 braney Fri Sep 4 12:47:16 2026 -0700 docent: nightly cron wrapper for the regression suite, refs #38252 Mails a report every night whether anything failed or not, matching the catalogNightly job: no mail means the cron has stopped, not that the browser is fine. The script mails on its own and always exits 0, so cron adds nothing. Two things it does deliberately: - It runs the COMMITTED tests, listed with git ls-files rather than by globbing the directory. The same directory holds scripts written against a ticket whose recipe is not right yet, and those must not mail a failure every night. A newly committed test is picked up with no edit. - It reports preflight separately from the tests, and preflight is now given an explicit script list (preflight.js takes names after the directory) so a dead fixture belonging to a work-in-progress script is not reported against a run that never included it. A missing session and a returning bug are different news and should not arrive as the same red. Verified: the pass path, the fail path (broke one committed assertion, restored it from git), and a run under `env -i` with only HOME and SHELL set, which is what cron will give it. Installed as `10 4 * * *` in braney's crontab, reading the tests out of the worktree because they are on this branch and not yet on master. It should move to a checkout of its own when the branch lands, so that a day's editing cannot change what the cron measures. diff --git src/hg/utils/docent/tests/preflight.js src/hg/utils/docent/tests/preflight.js index 5982e55eb98..1fbca9d6c12 100644 --- src/hg/utils/docent/tests/preflight.js +++ src/hg/utils/docent/tests/preflight.js @@ -1,34 +1,39 @@ #!/usr/bin/env node -/* preflight.js [DIR] +/* preflight.js [DIR] [SCRIPT...] * * Checks the things a directory of Docent tests depends on but does not contain: the * saved sessions the scripts load, the hubs they attach, the custom-track URLs they * fetch, and the server they all point at. No browser, so it runs in seconds and can be * run far more often than the suite it guards. * * It exists because of the way these tests fail without it. A saved session that has * been renamed or deleted produces no error: hgTracks answers 200 with a page titled * "Very Early Error" whose body reads "Could not find session NAME for user USER", the * page carries no track image, and every `noText:` assertion on it passes. The run goes * green while testing nothing. A hub URL that has moved behaves the same way. * * Fixtures are read out of the scripts rather than from a list kept beside them, so the * two cannot drift. A fixture named in no script is not checked, which is the point. * * Exit 0 if every fixture resolved, 1 otherwise, so a nightly run can tell "the fixtures * are gone" from "a bug came back". Without this they are the same red. + * + * With no script names it checks every *.docent.yaml in DIR, which is what you want + * interactively. Name scripts to check only those: the nightly run passes the COMMITTED + * list, because a directory also holds work in progress and a dead fixture belonging to + * a script that is not running should not be reported as a problem. */ 'use strict'; const fs = require('fs'); const path = require('path'); const yaml = require('js-yaml'); const DIR = process.argv[2] || '.'; const SERVERS = { 'rr': 'https://genome.ucsc.edu/cgi-bin', 'genome-test': 'https://genome-test.gi.ucsc.edu/cgi-bin', 'hgwdev': 'https://hgwdev.gi.ucsc.edu/cgi-bin', 'hgwbeta': 'https://hgwbeta.soe.ucsc.edu/cgi-bin', }; const resolveTarget = t => { if (!t) return SERVERS['genome-test']; @@ -63,31 +68,34 @@ }; } function urlCheck(url, wantText) { return async () => { const { status, body } = await get(url); if (status !== 200) return `HTTP ${status}`; if (!body.trim()) return 'empty response'; if (wantText && !body.includes(wantText)) return `no "${wantText}" in the response`; return null; }; } const fileCheck = file => async () => fs.existsSync(file) ? null : 'no such file'; -const scripts = fs.readdirSync(DIR).filter(f => f.endsWith('.docent.yaml')).sort(); +const named = process.argv.slice(3) + .map(a => a.endsWith('.docent.yaml') ? a : `${a}.docent.yaml`); +const scripts = (named.length ? named + : fs.readdirSync(DIR).filter(f => f.endsWith('.docent.yaml'))).sort(); const seenServer = new Map(); for (const f of scripts) { let doc; try { doc = yaml.load(fs.readFileSync(path.join(DIR, f), 'utf8')) || {}; } catch (e) { add({ script: f, kind: 'script', label: f, check: async () => `unreadable YAML: ${e.message}` }); continue; } const server = resolveTarget(doc.target).replace(/\/$/, ''); if (!seenServer.has(server)) seenServer.set(server, f); const base = path.basename(f, '.docent.yaml'); for (const step of (doc.steps || [])) {