on master help sign in
Add tools/check, one runner for every check

docs/development.md listed 67 commands to run before pushing, and the review (answer 30) asked for one tools/check that a CI job and a Nix flake can call. tools/check holds the one list: the 62 eval checks, which need only urbit and Python, and the 4 ship checks, which need the fake ~zod. `tools/golden --case types-prelude-ui` is left out, since `tools/golden` runs that case anyway. The eval checks run several at a time (-j, one per CPU by default), slowest first; the ship checks then run one at a time, ship-selftest before ship-test so that ship-test puts the real files back on the jibb-dev desk. tools/check starts the ship when it isn't running and stops it afterwards. A check passes when it exits 0, and the tools/hoon checks, which print their verdict and exit 0 either way, when they print the verdict that means a pass. Each check's output is kept and printed only when it fails; --logs DIR keeps it all, and --group, names, globs and --list pick and show the checks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U4FzmTL265QGvDaa6pxF4f

commit0a86d7caed2a4075fc8124d58c66def93a0de8db authorClaude <noreply@anthropic.com> dateMon 2 Nov 2026, 16:24 (10 hours ago) parent537afda
3 files changed+393−68
README.md+1−1 docs/development.md+7−67 tools/check+385
README.md+1−1
@@ -11,7 +11,7 @@ Milestone 1 of the doc's eight, the parser and the `%jibb` mark, is done; milest
1111- `my-app/`: an example app desk built from the doc's worked examples.
1212- `vendor/`: pinned copies of the base-dev and landscape files the desks need.
1313- `tests/`: golden cases in `golden/<case>/`, and on-ship tests for the `-test` thread in `lib/jibb.hoon`.
14−- `tools/`: dev tooling: `golden`, `hoon` and `doc-blocks`, with their self-test in `testdata/`, and `ship`.
14+- `tools/`: dev tooling: `golden`, `hoon` and `doc-blocks`, with their self-test in `testdata/`, `ship`, and `check`, which runs every check.
1515- `docs/`: the doc's tabs as Markdown in `spec/`, the status, the open questions, the development guide, the compiler's map, and briefs for milestones in `prompts/`.
1616
1717## Golden tests
docs/development.md+7−67
@@ -60,6 +60,7 @@ git clone https://github.com/urbit/tools /root/src/tools
6060
6161| Tool | What it does |
6262| --- | --- |
63+| `tools/check` | Runs every check in this table, or a group (`--group eval` or `ship`), or some by name: see [Before pushing](#before-pushing) |
6364| `tools/golden` | Runs the golden cases under `urbit eval`: every case, or those matching `--case`; `--update` rewrites the expected outputs; `--show` prints them |
6465| `tools/importer-test` | Compares the partial Hoon importer's diagnostic trees and errors with `tests/importer/tree.txt` under `urbit eval` |
6566| `tools/type-import-default-test` | Compares default-wrapped union source proofs and adversarial cases under eval; the same golden runs on the ship |
@@ -126,75 +127,14 @@ git clone https://github.com/urbit/tools /root/src/tools
126127## Before pushing
127128
128129```
129−tools/golden # all cases pass
130−tools/golden --case types-prelude-ui # exact UI/program/page schemes and composition
131−tools/importer-test # Hoon importer fixture projections match
132−tools/type-import-default-test # default-wrapped source proofs match
133−tools/type-import-test # importer/checker bridge golden report matches
134−tools/type-import-overlap-test # source-aware Hoon overlap and provenance controls
135−tools/type-import-structural-test # nested and multi-case Hoon overlap controls
136−tools/type-union-source-test # local generic-alias union guards
137−tools/type-union-alias-overlap-test # local generic-alias overlap diagnostics
138−tools/type-full-overlap-test # full-module overlap evidence
139−tools/type-tlon-test # real Tlon importer/checker report matches
140−tools/type-import-diagnostic-test # direct structured reader diagnostics match
141−tools/type-mark-test # mark/checker bridge golden report matches
142−tools/type-poke-test # source-backed poke/API bridge report matches
143−tools/type-api-paths-test # partial API path analysis report matches
144−tools/type-api-evidence-test # API declaration evidence guards pass
145−tools/type-hole-guards-test # hole defaulting and API guards pass
146−tools/type-list-alias-test # prelude list-alias guards pass
147−tools/type-generic-alias-test # generic union alias guards pass
148−tools/type-positional-face-guards-test # positional sample face guards pass
149−tools/type-qath-guards-test # qath source and unfolding guards pass
150−tools/type-todo-partial-test # unchanged todo partial report and reader failure controls
151−tools/type-todo-api-test # real todo API partial inference and reader guards pass
152−tools/type-direct-gate-test # direct gate source and provenance guards pass
153−tools/type-page-main-test # static/data pages, exact types, imports and rejection guards pass
154−tools/type-buttons-test # documented buttons schemes and exact pending-tag error
155−tools/type-doc-examples-test # remaining doc fragments fully check in context
156−tools/type-theme-test # real theme, exact type, structural import and rejection guards pass
157−tools/type-program-main-test # closed program-main interface and rejection guards pass
158−tools/type-program-init-test # transactional direct-program initializer context guards pass
159−tools/type-program-update-test # transactional direct-program update context guards pass
160−tools/type-program-watch-test # direct request-value watchability guards pass
161−tools/type-store-test # unchanged cart/basket, store evidence and rejection guards pass
162−tools/type-todo-full-test # byte-identical full todo, downstream import and rejection guards pass
163−tools/type-generic-recursion-test # source-backed recursive identities and provenance
164−tools/type-generic-construction-test # source-backed recursive construction and bounds
165−tools/type-generic-pattern-test # source-backed recursive patterns and bounds
166−tools/type-deferred-test # source-owned deferred inference guards
167−tools/type-deferred-resolver-test # resolver consistency and failure guards
168−tools/type-deferred-context-test # deferred source-order and branch guards
169−tools/type-deferred-prepared-test # prepared initializer reports and failure guards
170−tools/type-cell-branch-test # contextual branch rollback and bounds
171−tools/type-closed-gate-guards-test # closed gate fit guards pass
172−tools/type-constructor-fit-guards-test # constructor fit guards pass
173−tools/type-nested-union-guards-test # nested union selector guards pass
174−tools/type-empty-test # empty mold and uninhabited-case guards pass
175−tools/type-gate-free-guards-test # bounded gate-free classifier guards pass
176−tools/type-gate-free-source-test # real todo mold analysis and source guards pass
177−tools/type-sub-guards-test # subscription source guards pass
178−tools/type-ask-guards-test # ask source and bootstrap guards pass
179−tools/type-request-guards-test # request evidence and cache guards pass
180−tools/type-proj-guards-test # projection evidence and scope guards pass
181−tools/type-arithmetic-guards-test # arithmetic constraint guards pass
182−tools/type-module-scope-test # module/prelude scope golden report matches
183−tools/hoon check-spans-test # prints <<>>
184−tools/hoon itype-unify-test # type-unification controls pass
185−tools/hoon -f tools/testdata/zing-hoon-reference.hoon # Hoon translation agrees on four inputs
186−tools/hoon -f tools/testdata/lead-late-hoon-reference.hoon # Hoon translation agrees on selected inputs
187−tools/hoon -f tools/testdata/succ-hoon-reference.hoon # Hoon translation agrees on selected inputs
188−tools/testdata/selftest # 22 checks, 0 failed
189−tools/doc-blocks docs/spec # every jibb block has its case
190−tools/ship gen-tests --check # tests/lib/jibb.hoon is up to date
191−tools/ship test # the cases pass as -test arms on the ship
192−tools/testdata/ship-selftest # when tools/ship changed
193−tools/testdata/clay-fixtures-selftest # scratch fixture cleanup without a ship
194−tools/ship check-commit # when the mark or the desks changed
195−tools/clay-fixtures # builds example fixtures and real Tlon notes through Clay
130+tools/check # every check; it exits 0 only when all pass
131+tools/check --group eval # only the checks that need no ship, as CI runs them
196132```
197133
134+`tools/check` runs every check the repo has. Their one list is `CHECKS` in the script, so a new check goes there, as well as in the table above; `tools/check --list` prints it with each check's command. The eval group, the 62 checks that need only `urbit` and Python, runs several at a time (`-j`, by default one per CPU), the slowest first. The ship group, `ship-selftest`, `ship-test`, `check-commit` and `clay-fixtures`, then runs one check at a time on the ship `tools/ship` talks to; `tools/check` starts it if it isn't running and stops it again afterwards. Name checks, or globs, to run only those: `tools/check golden 'type-todo-*'`. A failing check's output follows its FAIL line; `-v` prints every check's output, and `--logs DIR` keeps it all.
135+
136+`ship-selftest` matters most when `tools/ship` changed, and `check-commit` when the mark or the desks changed, but `tools/check` runs both every time. The self-test leaves fixture files on the `jibb-dev` desk, and `ship-test`, which runs after it, puts the real ones back.
137+
198138`tools/ship test` regenerates `tests/lib/jibb.hoon` and syncs the `jibb-dev` desk before it runs, so commit the regenerated file. Sync preflights and splits large deletion lists into bounded click requests, waiting for each Clay revision; a failure stops later batches. The ship self-test exercises this when switching from the full desk to a smaller fixture desk. A run with `--stub`, `--lib`, or a custom `--golden` isolates its generated golden tests from the repository's other test programs; run the default command afterward to restore and test those programs. It also runs the independent `tests/lib/jibb-import.hoon` test against the same importer fixtures and expected diagnostic projection as `tools/importer-test`. The projection is an internal tree, not the Jibb type printer. `tests/lib/jibb-bridge.hoon` also runs the importer/checker bridge fixture on the ship; `tools/type-import-test` runs the same report under eval. `tests/lib/jibb-tlon-bridge.hoon` and `tools/type-tlon-test` run the real Tlon presence reader/checker fixture on both. The structured reader diagnostics have their own `tests/lib/jibb-import-diagnostic.hoon` ship test and `tools/type-import-diagnostic-test` eval test. The mark bridge has its own `tests/lib/jibb-mark-bridge.hoon` ship test and `tools/type-mark-test` eval test; both compare the same fixture report. `tests/lib/jibb-poke-bridge.hoon` and `tools/type-poke-test` share the source-backed prelude and poke-only API fixture. `tests/lib/jibb-api-paths.hoon` and `tools/type-api-paths-test` share partial path analysis over the real todo marks; this does not generate request interfaces. `tests/lib/jibb-module-scope.hoon` and `tools/type-module-scope-test` compare the prelude scope report on the ship and under eval.
199139
200140## Golden cases
tools/checknew file+385
@@ -0,0 +1,385 @@
1+#!/usr/bin/env python3
2+"""Run the repo's checks: all of them, a group, or some by name.
3+
4+ tools/check every check: the eval group, then ship
5+ tools/check --group eval the checks that need only urbit and Python
6+ tools/check --group ship the checks that need the fake ~zod
7+ tools/check golden 'type-*' only the checks these names or globs match
8+ tools/check --list the checks, their groups and commands
9+ tools/check -j 3 at most 3 eval checks at once
10+
11+CHECKS below is the one list of the repo's checks: docs/development.md
12+sends you here before pushing, and `nix flake check` runs this tool too
13+(see flake.nix), so a new check goes in the list and nowhere else.
14+
15+The eval checks run under `urbit eval` without a ship, several at once
16+(-j, default: as many as there are CPUs), in the order of the list, so
17+the slowest come first. The ship checks then run one at a time, in
18+order, on the ship tools/ship talks to: $JIBB_PIER, or /root/urbit/zod
19+(tools/ship also reads $JIBB_CLICK and $JIBB_SHIP_LOG). When that ship
20+isn't running, tools/check starts it first and stops it again at the end.
21+
22+Each check is one command, run from the repo root. It passes when it
23+exits 0 and, for the few that print their verdict rather than exit with
24+it, when its output matches what the list expects. Output is kept and
25+printed only for a check that fails (all of it with -v); --logs DIR
26+keeps every check's output in DIR/<name>.log.
27+
28+Exit status: 0 when every check ran and passed, 1 when a check failed,
29+2 when the checks couldn't run (an unknown name, a ship that won't start).
30+"""
31+
32+import argparse
33+import concurrent.futures
34+import fnmatch
35+import os
36+import re
37+import shutil
38+import signal
39+import subprocess
40+import sys
41+import tempfile
42+import threading
43+import time
44+
45+ROOT = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
46+SHIP = os.path.join('tools', 'ship')
47+
48+
49+class Check:
50+ def __init__(self, name, group, command, expect=None):
51+ self.name = name
52+ self.group = group # 'eval' or 'ship'
53+ self.command = command # argv, run from ROOT
54+ self.expect = expect # a regex stdout must match in full
55+
56+ def shown(self):
57+ return ' '.join(c if re.match(r'^[\w./=-]+$', c) else "'%s'" % c
58+ for c in self.command)
59+
60+
61+def tool(name, group='eval', *args):
62+ """A check that runs tools/<name> and passes when it exits 0."""
63+ base = os.path.basename(name)
64+ return Check(base, group, [os.path.join('tools', name)] + list(args))
65+
66+
67+def hoon(name, *args, expect):
68+ """A check that runs tools/hoon and compares what it prints."""
69+ return Check(name, 'eval', ['tools/hoon'] + list(args), expect=expect)
70+
71+
72+# Every check, in the order they start: the eval group, slowest first,
73+# then the ship group, in the order they must run. Times are from one
74+# run of every check, three at a time on four CPUs (2026-11-02).
75+CHECKS = [
76+ tool('type-todo-full-test'), # about 580 s
77+ tool('type-deferred-prepared-test'), # about 410 s
78+ tool('golden'),
79+ tool('importer-test'),
80+ tool('type-import-default-test'),
81+ tool('type-import-test'),
82+ tool('type-import-overlap-test'),
83+ tool('type-import-structural-test'),
84+ tool('type-union-source-test'),
85+ tool('type-union-alias-overlap-test'),
86+ tool('type-full-overlap-test'),
87+ tool('type-tlon-test'),
88+ tool('type-import-diagnostic-test'),
89+ tool('type-mark-test'),
90+ tool('type-poke-test'),
91+ tool('type-api-paths-test'),
92+ tool('type-api-evidence-test'),
93+ tool('type-hole-guards-test'),
94+ tool('type-list-alias-test'),
95+ tool('type-generic-alias-test'),
96+ tool('type-positional-face-guards-test'),
97+ tool('type-qath-guards-test'),
98+ tool('type-todo-partial-test'),
99+ tool('type-todo-api-test'),
100+ tool('type-direct-gate-test'),
101+ tool('type-page-main-test'),
102+ tool('type-buttons-test'),
103+ tool('type-doc-examples-test'),
104+ tool('type-theme-test'),
105+ tool('type-program-main-test'),
106+ tool('type-program-init-test'),
107+ tool('type-program-update-test'),
108+ tool('type-program-watch-test'),
109+ tool('type-store-test'),
110+ tool('type-generic-recursion-test'),
111+ tool('type-generic-construction-test'),
112+ tool('type-generic-pattern-test'),
113+ tool('type-deferred-test'),
114+ tool('type-deferred-resolver-test'),
115+ tool('type-deferred-context-test'),
116+ tool('type-cell-branch-test'),
117+ tool('type-closed-gate-guards-test'),
118+ tool('type-constructor-fit-guards-test'),
119+ tool('type-nested-union-guards-test'),
120+ tool('type-empty-test'),
121+ tool('type-gate-free-guards-test'),
122+ tool('type-gate-free-source-test'),
123+ tool('type-sub-guards-test'),
124+ tool('type-ask-guards-test'),
125+ tool('type-request-guards-test'),
126+ tool('type-proj-guards-test'),
127+ tool('type-arithmetic-guards-test'),
128+ tool('type-module-scope-test'),
129+ # These print their verdict and exit 0 either way.
130+ hoon('check-spans-test', 'check-spans-test', expect=r'<<>>\n'),
131+ hoon('itype-unify-test', 'itype-unify-test', expect=r'<<>>\n'),
132+ hoon('zing-hoon-reference', '-f',
133+ 'tools/testdata/zing-hoon-reference.hoon',
134+ expect=r"'Hoon flattening reference: \d+ cases passed'\n"),
135+ hoon('lead-late-hoon-reference', '-f',
136+ 'tools/testdata/lead-late-hoon-reference.hoon',
137+ expect=r"'Hoon lead/late reference: \d+ comparisons passed'\n"),
138+ hoon('succ-hoon-reference', '-f',
139+ 'tools/testdata/succ-hoon-reference.hoon',
140+ expect=r"'Hoon successor reference: \d+ comparisons passed'\n"),
141+ tool('testdata/selftest'),
142+ Check('doc-blocks', 'eval', ['tools/doc-blocks', 'docs/spec']),
143+ Check('gen-tests', 'eval', [SHIP, 'gen-tests', '--check']),
144+ tool('testdata/clay-fixtures-selftest'),
145+ # The ship group. ship-selftest leaves its fixture files on the
146+ # jibb-dev desk, and ship-test, which syncs the desk to the repo
147+ # first, puts the real ones back, so it must come after.
148+ tool('testdata/ship-selftest', 'ship'),
149+ Check('ship-test', 'ship', [SHIP, 'test']),
150+ Check('check-commit', 'ship', [SHIP, 'check-commit']),
151+ tool('clay-fixtures', 'ship'),
152+]
153+
154+GROUPS = ('eval', 'ship')
155+
156+
157+# ---------------------------------------------------------------- running
158+
159+
160+class Result:
161+ def __init__(self, check):
162+ self.check = check
163+ self.ok = False
164+ self.status = None
165+ self.why = ''
166+ self.output = ''
167+ self.seconds = 0.0
168+
169+
170+_lock = threading.Lock()
171+_running = set() # process groups to kill if we're interrupted
172+
173+
174+def say(msg=''):
175+ with _lock:
176+ print(msg, flush=True)
177+
178+
179+def run(check, timeout, env):
180+ """Run one check; a Result."""
181+ res = Result(check)
182+ start = time.time()
183+ with tempfile.TemporaryFile() as out, tempfile.TemporaryFile() as err:
184+ try:
185+ proc = subprocess.Popen(
186+ check.command, cwd=ROOT, env=env, stdin=subprocess.DEVNULL,
187+ stdout=out,
188+ stderr=err if check.expect is not None else subprocess.STDOUT,
189+ start_new_session=True)
190+ except OSError as e:
191+ res.why = 'could not run %s: %s' % (check.command[0], e)
192+ return res
193+ with _lock:
194+ _running.add(proc.pid)
195+ try:
196+ res.status = proc.wait(timeout=timeout)
197+ except subprocess.TimeoutExpired:
198+ kill(proc.pid)
199+ proc.wait()
200+ res.why = 'timed out after %d s' % timeout
201+ finally:
202+ with _lock:
203+ _running.discard(proc.pid)
204+ kill(proc.pid) # anything it left running, such as urbit eval
205+ out.seek(0)
206+ err.seek(0)
207+ stdout = out.read().decode('utf-8', 'replace')
208+ stderr = err.read().decode('utf-8', 'replace')
209+ res.seconds = time.time() - start
210+ res.output = stdout + stderr
211+ if res.why:
212+ return res
213+ if res.status != 0:
214+ res.why = 'exit status %d' % res.status
215+ elif check.expect is not None and \
216+ not re.fullmatch(check.expect, stdout):
217+ res.why = "it didn't print the verdict that means a pass"
218+ else:
219+ res.ok = True
220+ return res
221+
222+
223+def kill(pgid):
224+ try:
225+ os.killpg(pgid, signal.SIGKILL)
226+ except (ProcessLookupError, PermissionError):
227+ pass
228+
229+
230+def kill_all(*_):
231+ with _lock:
232+ pids = list(_running)
233+ for pid in pids:
234+ kill(pid)
235+
236+
237+def report(res, verbose, logs, limit=300):
238+ mark = 'PASS' if res.ok else 'FAIL'
239+ line = '%s %-34s %5d s' % (mark, res.check.name, round(res.seconds))
240+ if not res.ok:
241+ line += ' (%s)' % res.why
242+ lines = res.output.rstrip('\n').split('\n') if res.output.strip() else []
243+ if logs:
244+ with open(os.path.join(logs, res.check.name + '.log'), 'w') as f:
245+ f.write('$ %s\n%s' % (res.check.shown(), res.output))
246+ if res.ok and not verbose:
247+ say(line)
248+ return
249+ shown = lines
250+ if not verbose and len(lines) > limit:
251+ shown = ['(%d lines cut; --logs DIR keeps them all)'
252+ % (len(lines) - limit)] + lines[-limit:]
253+ body = [' $ ' + res.check.shown()] + [' ' + l for l in shown]
254+ say('\n'.join([line] + body))
255+
256+
257+# ---------------------------------------------------------------- the ship
258+
259+
260+def ship(env, *args, **kw):
261+ return subprocess.run([SHIP] + list(args), cwd=ROOT, env=env, **kw)
262+
263+
264+def ship_running(env):
265+ """Whether tools/ship's ship runs: status exits 3 when it doesn't."""
266+ res = ship(env, 'status', stdout=subprocess.DEVNULL,
267+ stderr=subprocess.DEVNULL)
268+ return res.returncode != 3
269+
270+
271+# ---------------------------------------------------------------- main
272+
273+
274+def select(names, group):
275+ """The checks to run, in list order; exits 2 on a name nothing matches."""
276+ chosen = [c for c in CHECKS if group in (None, c.group)]
277+ if not names:
278+ return chosen
279+ picked = set()
280+ for name in names:
281+ hits = [c.name for c in chosen if fnmatch.fnmatchcase(c.name, name)]
282+ if not hits:
283+ where = ' in the %s group' % group if group else ''
284+ print('check: no check %s%s; tools/check --list lists them'
285+ % (name, where), file=sys.stderr)
286+ sys.exit(2)
287+ picked.update(hits)
288+ return [c for c in chosen if c.name in picked]
289+
290+
291+def main(argv):
292+ ap = argparse.ArgumentParser(
293+ prog='tools/check', description=__doc__.split('\n\n')[0],
294+ formatter_class=argparse.RawDescriptionHelpFormatter,
295+ epilog='\n\n'.join(__doc__.split('\n\n')[1:]))
296+ ap.add_argument('names', nargs='*', metavar='NAME',
297+ help='checks to run, by name or glob (default: all)')
298+ ap.add_argument('--group', choices=GROUPS,
299+ help='only the checks in this group')
300+ ap.add_argument('--list', action='store_true',
301+ help='list the checks and exit')
302+ ap.add_argument('-j', '--jobs', type=int, default=os.cpu_count() or 1,
303+ metavar='N', help='eval checks to run at once '
304+ '(default: the number of CPUs, %(default)s)')
305+ ap.add_argument('--timeout', type=int, default=2400, metavar='SECONDS',
306+ help='seconds each check may take (default %(default)s)')
307+ ap.add_argument('--logs', metavar='DIR',
308+ help="keep every check's output in DIR/<name>.log")
309+ ap.add_argument('-v', '--verbose', action='store_true',
310+ help="print every check's output, not only a failure's")
311+ args = ap.parse_args(argv)
312+
313+ checks = select(args.names, args.group)
314+ if args.list:
315+ for c in checks:
316+ print('%-34s %-5s %s' % (c.name, c.group, c.shown()))
317+ return 0
318+ if args.jobs < 1:
319+ ap.error('-j must be at least 1')
320+ if args.logs:
321+ os.makedirs(args.logs, exist_ok=True)
322+
323+ env = dict(os.environ, PYTHONDONTWRITEBYTECODE='1')
324+ signal.signal(signal.SIGTERM, lambda *_: (kill_all(), sys.exit(143)))
325+ started = time.time()
326+ results = []
327+ unable = False
328+ try:
329+ evals = [c for c in checks if c.group == 'eval']
330+ if evals:
331+ say('running %d eval check%s, %d at a time'
332+ % (len(evals), '' if len(evals) == 1 else 's', args.jobs))
333+ with concurrent.futures.ThreadPoolExecutor(args.jobs) as pool:
334+ futures = [pool.submit(run, c, args.timeout, env)
335+ for c in evals]
336+ for f in concurrent.futures.as_completed(futures):
337+ res = f.result()
338+ results.append(res)
339+ report(res, args.verbose, args.logs)
340+ ships = [c for c in checks if c.group == 'ship']
341+ if ships:
342+ started_ship = False
343+ if not ship_running(env):
344+ say('starting the ship for %d ship check%s'
345+ % (len(ships), '' if len(ships) == 1 else 's'))
346+ start = ship(env, 'start')
347+ if start.returncode != 0:
348+ say('check: the ship would not start, so the ship '
349+ 'checks did not run')
350+ unable = True
351+ ships = []
352+ started_ship = not unable
353+ else:
354+ say('running %d ship check%s on the running ship'
355+ % (len(ships), '' if len(ships) == 1 else 's'))
356+ try:
357+ for c in ships:
358+ res = run(c, args.timeout, env)
359+ results.append(res)
360+ report(res, args.verbose, args.logs)
361+ finally:
362+ if started_ship:
363+ say('stopping the ship tools/check started')
364+ ship(env, 'stop', '--force')
365+ except KeyboardInterrupt:
366+ kill_all()
367+ print('check: interrupted', file=sys.stderr)
368+ return 130
369+
370+ failed = [r for r in results if not r.ok]
371+ order = {c.name: i for i, c in enumerate(CHECKS)}
372+ say('')
373+ say('%d passed, %d failed%s in %d s' % (
374+ len(results) - len(failed), len(failed),
375+ ', ship checks not run' if unable else '',
376+ round(time.time() - started)))
377+ for r in sorted(failed, key=lambda r: order[r.check.name]):
378+ say(' failed: %s (%s)' % (r.check.name, r.why))
379+ if failed:
380+ return 1
381+ return 2 if unable else 0
382+
383+
384+if __name__ == '__main__':
385+ sys.exit(main(sys.argv[1:]))