rvlib-shipkit 0.38.0 → 1.0.0
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/CHANGELOG.md +371 -16
- package/README.md +26 -14
- package/package.json +2 -2
- package/src/assets/gen/comfyts-gen.ts +27 -10
- package/src/assets/gen/generate-roll.ts +1 -1
- package/src/assets/kit-config.ts +6 -6
- package/src/assets/run-assets.ts +14 -10
- package/src/assets/server/studio-server.ts +6 -5
- package/src/check.ts +343 -0
- package/src/{rules/SK001-package-json-shape.ts → checks/SK001-package-json-shape.check.ts} +44 -46
- package/src/checks/SK002-contracts-fulfillness.check.ts +24 -0
- package/src/checks/SK003-claude-md-shape.check.ts +66 -0
- package/src/checks/SK004-gitignore-policy.check.ts +309 -0
- package/src/{rules/SK005-rvlib-versions.ts → checks/SK005-rvlib-versions.check.ts} +46 -32
- package/src/{rules/SK006-npm-publish-safety.ts → checks/SK006-npm-publish-safety.check.ts} +25 -24
- package/src/{rules/SK007-github-visibility.ts → checks/SK007-github-visibility.check.ts} +20 -14
- package/src/{rules/SK008-reviewer-agent.ts → checks/SK008-reviewer-agent.check.ts} +29 -14
- package/src/{rules/SK009-commit-ai-tells.ts → checks/SK009-commit-ai-tells.check.ts} +24 -21
- package/src/checks/SK010-undeclared-unit.check.ts +111 -0
- package/src/{rules/SK011-sibling-unit-ranges.ts → checks/SK011-sibling-unit-ranges.check.ts} +21 -20
- package/src/{rules/SK012-config-barrel-import.ts → checks/SK012-config-barrel-import.check.ts} +18 -23
- package/src/{rules/SK013-one-way-in-policy.ts → checks/SK013-one-way-in-policy.check.ts} +58 -50
- package/src/{rules/SK014-prose-ste.ts → checks/SK014-prose-ste.check.ts} +27 -13
- package/src/{rules/SK015-nested-repo.ts → checks/SK015-nested-repo.check.ts} +15 -12
- package/src/checks/SK016-legacy-check-shapes.check.ts +182 -0
- package/src/checks/SK017-vscode-hub-task.check.ts +197 -0
- package/src/checks/SK018-test-files-run.check.ts +28 -0
- package/src/checks/SK019-tests-run-in-units.check.ts +76 -0
- package/src/checks/SK020-self-spawning-check.check.ts +147 -0
- package/src/checks/contracts.check.ts +20 -0
- package/src/cli/cmd_Asks.ts +3 -2
- package/src/cli/cmd_Assets.ts +2 -2
- package/src/cli/cmd_Bump.ts +1 -1
- package/src/cli/cmd_Check.ts +6 -6
- package/src/cli/cmd_Checks.ts +95 -0
- package/src/cli/cmd_Ci.ts +48 -13
- package/src/cli/cmd_Commands.ts +2 -6
- package/src/cli/cmd_ConfigKeys.ts +69 -0
- package/src/cli/cmd_Contracts.ts +1 -1
- package/src/cli/cmd_Fleet.ts +6 -2
- package/src/cli/cmd_Hooks.ts +1 -1
- package/src/cli/cmd_Keys.ts +43 -18
- package/src/cli/cmd_Mv.ts +2 -2
- package/src/cli/cmd_Prose.ts +10 -1
- package/src/cli/cmd_Releases.ts +2 -2
- package/src/cli/cmd_Serve.ts +1 -1
- package/src/cli/cmd_Start.ts +6 -6
- package/src/cli/cmd_Status.ts +25 -0
- package/src/cli/cmd_Tui.ts +2 -2
- package/src/cli.ts +7 -4
- package/src/collections/actions.ts +4 -4
- package/src/collections/checks.ts +258 -0
- package/src/collections/collection-types.ts +254 -1
- package/src/collections/comfy.ts +23 -0
- package/src/collections/md-body.ts +14 -0
- package/src/collections/pings.ts +38 -12
- package/src/collections/posts.ts +19 -3
- package/src/collections/providers.ts +198 -91
- package/src/collections/reflections.ts +41 -0
- package/src/collections/state-rules.ts +73 -0
- package/src/collections/test-time.ts +288 -0
- package/src/collections/tests.ts +181 -0
- package/src/collections/wikis.ts +66 -0
- package/src/comfy/comfy-panel.ts +292 -0
- package/src/contracts/apply-contracts.ts +17 -12
- package/src/contracts/contract-types.ts +16 -4
- package/src/contracts/contracts-engine.ts +78 -42
- package/src/contracts/manifests/discord.contract.ts +6 -6
- package/src/contracts/manifests/identity.contract.ts +3 -3
- package/src/contracts/manifests/itch.contract.ts +12 -15
- package/src/contracts/manifests/social.contract.ts +5 -5
- package/src/contracts/manifests/steam.contract.ts +16 -16
- package/src/contracts/run-contracts.ts +4 -3
- package/src/core/attach.ts +96 -0
- package/src/core/canonical-scripts.ts +25 -0
- package/src/core/check-ctx.ts +204 -14
- package/src/core/check-file-names.ts +112 -0
- package/src/core/check-pass.ts +84 -0
- package/src/core/check-runner.ts +796 -0
- package/src/core/config-keys.ts +256 -0
- package/src/core/config-rewrite.ts +77 -0
- package/src/core/discover-checks.ts +166 -90
- package/src/core/discover-legacy-checks.ts +26 -0
- package/src/core/doc-block.ts +13 -0
- package/src/core/floor-resolve.ts +264 -0
- package/src/core/floor.ts +20 -137
- package/src/core/git-bin.ts +24 -0
- package/src/core/git-root.ts +108 -0
- package/src/core/instance.ts +195 -0
- package/src/core/jobs.ts +118 -0
- package/src/core/layout.ts +52 -0
- package/src/core/legacy-shapes.ts +207 -0
- package/src/core/lib-root.ts +5 -0
- package/src/core/locked-write.ts +110 -39
- package/src/core/machine-commit.ts +5 -4
- package/src/core/migrate-layout.ts +557 -0
- package/src/core/motherhome.ts +2 -8
- package/src/core/nesting.ts +4 -12
- package/src/core/one-way-in-policy.ts +73 -17
- package/src/core/outcome-words.ts +45 -0
- package/src/core/package-manager.ts +86 -0
- package/src/core/parallel.ts +21 -0
- package/src/core/process-tree.ts +44 -0
- package/src/core/raw-access.ts +93 -0
- package/src/core/read-set.ts +152 -0
- package/src/core/run-check.ts +155 -213
- package/src/core/run-ci.ts +153 -189
- package/src/core/run-fix.ts +96 -0
- package/src/core/run-fixers.ts +53 -30
- package/src/core/self-update.ts +9 -3
- package/src/core/slots.ts +83 -0
- package/src/core/spawn-timeout.ts +34 -3
- package/src/core/when-block.ts +401 -0
- package/src/core/when.ts +135 -0
- package/src/core/write-status.ts +5 -2
- package/src/hooks/leak-guard.ts +10 -8
- package/src/hooks/one-way-in-guard.ts +13 -5
- package/src/hooks/root-identity.ts +2 -1
- package/src/hooks/run-hooks.ts +35 -23
- package/src/init-repo.ts +39 -23
- package/src/ledger/check-stats.ts +155 -0
- package/src/ledger/check-table.ts +179 -0
- package/src/ledger/db.ts +255 -0
- package/src/ledger/gate-checks.ts +433 -0
- package/src/ledger/git-key.ts +116 -77
- package/src/ledger/git-scan.ts +96 -35
- package/src/ledger/ledger.ts +116 -42
- package/src/ledger/live-runs.ts +111 -0
- package/src/ledger/live-timeline.ts +97 -0
- package/src/ledger/repo-view.ts +73 -0
- package/src/ledger/run-log.ts +448 -0
- package/src/ledger/run-timeline.ts +173 -0
- package/src/ledger/store.ts +23 -12
- package/src/ledger/suite-log.ts +161 -0
- package/src/ledger/toolchain-identity.ts +7 -1
- package/src/ledger/world-cache.ts +111 -0
- package/src/mv/mv-md.ts +16 -6
- package/src/problems/extract.ts +182 -0
- package/src/problems/problem-board.ts +37 -0
- package/src/problems/problem-line.ts +77 -0
- package/src/prose/prose-view.ts +90 -0
- package/src/prose/run-prose.ts +60 -19
- package/src/prose/ste.ts +140 -24
- package/src/reflections/reflections-queue.ts +81 -0
- package/src/release/git-facts.ts +15 -9
- package/src/release/run-bump.ts +6 -5
- package/src/release/run-release.ts +25 -11
- package/src/release/run-releases.ts +2 -2
- package/src/release/unit-gate.ts +7 -54
- package/src/release/unit-resolve.ts +282 -0
- package/src/release/units.ts +33 -217
- package/src/repo-config.ts +155 -85
- package/src/rules/rule.ts +36 -96
- package/src/rules/untrack-generated.ts +5 -4
- package/src/scaffold/files/vscode-tasks.json +10 -31
- package/src/scaffold/jsonc-spans.ts +113 -0
- package/src/scaffold/project-scaffold.ts +16 -43
- package/src/scan.ts +98 -0
- package/src/serve/gate-trigger.ts +148 -0
- package/src/serve/health-schedule.ts +102 -0
- package/src/serve/hub-app/chart-view.ts +179 -0
- package/src/serve/hub-app/check-view.tsx +345 -0
- package/src/serve/hub-app/check-words.ts +164 -0
- package/src/serve/hub-app/checks-svc.ts +280 -0
- package/src/serve/hub-app/clock-svc.ts +33 -0
- package/src/serve/hub-app/col-card.tsx +23 -39
- package/src/serve/hub-app/cols-svc.ts +44 -20
- package/src/serve/hub-app/comfy-svc.ts +171 -0
- package/src/serve/hub-app/comfy-view.tsx +81 -0
- package/src/serve/hub-app/dur.ts +41 -0
- package/src/serve/hub-app/hash-route.ts +121 -0
- package/src/serve/hub-app/header-bar.tsx +117 -0
- package/src/serve/hub-app/health-svc.ts +23 -28
- package/src/serve/hub-app/help.tsx +29 -0
- package/src/serve/hub-app/hub-app.tsx +122 -119
- package/src/serve/hub-app/hub-st.ts +115 -42
- package/src/serve/hub-app/hub-types.ts +117 -2
- package/src/serve/hub-app/hub.css +534 -192
- package/src/serve/hub-app/index.html +2 -0
- package/src/serve/hub-app/live-svc.ts +174 -0
- package/src/serve/hub-app/md.ts +15 -0
- package/src/serve/hub-app/media-svc.ts +4 -1
- package/src/serve/hub-app/media-view.tsx +15 -0
- package/src/serve/hub-app/next-run.ts +68 -0
- package/src/serve/hub-app/project-gutter.css +65 -0
- package/src/serve/hub-app/project-gutter.tsx +128 -0
- package/src/serve/hub-app/projects-svc.ts +226 -0
- package/src/serve/hub-app/prose-hits-st.ts +124 -0
- package/src/serve/hub-app/prose-hits-view.tsx +176 -0
- package/src/serve/hub-app/prose-hits.css +21 -0
- package/src/serve/hub-app/run-strip.ts +47 -0
- package/src/serve/hub-app/runs-tabs.ts +24 -0
- package/src/serve/hub-app/test-view.tsx +107 -0
- package/src/serve/hub-app/tests-svc.ts +62 -0
- package/src/serve/hub-app/tests-view.tsx +238 -0
- package/src/serve/hub-app/time-ago.tsx +34 -0
- package/src/serve/hub-app/timeline-chart.tsx +274 -0
- package/src/serve/hub-app/timeline-geometry.ts +314 -0
- package/src/serve/hub-app/timeline-svc.ts +62 -0
- package/src/serve/hub-app/timeline-view.tsx +245 -0
- package/src/serve/hub-app/tree-build.ts +410 -0
- package/src/serve/hub-app/tree-row.tsx +42 -0
- package/src/serve/hub-app/tree-svc.ts +174 -0
- package/src/serve/hub-app/wikis-view.tsx +53 -0
- package/src/serve/other-repos.ts +79 -0
- package/src/serve/project-gutter-route.ts +350 -0
- package/src/serve/project-gutter.ts +449 -0
- package/src/serve/run-serve.ts +245 -16
- package/src/serve/same-origin.ts +14 -0
- package/src/serve/serve-hub.ts +213 -18
- package/src/shipkit.ts +19 -12
- package/src/smoke/run-smoke.ts +3 -2
- package/src/start-repo.ts +75 -36
- package/src/suite/judge-suites.ts +216 -18
- package/src/suite/kept-timings.ts +58 -0
- package/src/suite/runner-shapes.ts +88 -0
- package/src/suite/suite-cli.ts +147 -24
- package/src/suite/suite-cpu-preload.ts +92 -0
- package/src/suite/test-units.ts +243 -0
- package/src/ts/config.ts +4 -69
- package/src/ts/jsonc.ts +69 -0
- package/src/ts/lex.ts +143 -0
- package/src/ts/rename.ts +73 -10
- package/src/ts/source-graph.ts +28 -8
- package/src/ts/tsconfig-projects.ts +6 -97
- package/src/ts/tsconfig-resolve.ts +108 -0
- package/src/tui/build-steps-panel.ts +61 -0
- package/src/tui/checks-panel.ts +24 -16
- package/src/tui/run-tui.ts +2 -2
- package/src/tui/tui-panel.ts +5 -4
- package/src/wiki/graph-colors.ts +68 -0
- package/src/wiki/graph-mount.ts +7 -9
- package/src/wiki/graph-page.ts +42 -25
- package/src/wiki/graph-positions.ts +105 -0
- package/src/wiki/run-wiki.ts +1 -1
- package/src/wiki/serve-graph.ts +33 -3
- package/src/cli/cmd_Rules.ts +0 -51
- package/src/cli/cmd_Units.ts +0 -82
- package/src/collections/graphs.ts +0 -32
- package/src/contracts/contracts-once.ts +0 -43
- package/src/core/check-suite.ts +0 -302
- package/src/core/repair-contract-dirs.ts +0 -136
- package/src/core/task-runner.ts +0 -339
- package/src/ledger/gate-units.ts +0 -138
- package/src/ledger/unit-table.ts +0 -105
- package/src/rules/SK002-contracts-fulfillness.ts +0 -24
- package/src/rules/SK003-claude-md-shape.ts +0 -70
- package/src/rules/SK004-gitignore-policy.ts +0 -238
- package/src/rules/SK010-undeclared-release-unit.ts +0 -127
- package/src/rules/rule-registry.ts +0 -64
- package/src/rules/run-fix.ts +0 -72
- package/src/tui/tasks-panel.ts +0 -60
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { isAbsolute, posix } from 'path'
|
|
5
5
|
|
|
6
6
|
export type SlowSuite = {
|
|
7
|
+
/** the suite path, relative to the repo root (or to the unit dir inside `units[]`) */
|
|
7
8
|
file: string
|
|
8
9
|
/** what this suite catches that no fast test can */
|
|
9
10
|
catches: string
|
|
@@ -11,12 +12,13 @@ export type SlowSuite = {
|
|
|
11
12
|
whyNotFast: string
|
|
12
13
|
}
|
|
13
14
|
export type SerialSuite = {
|
|
15
|
+
/** the suite path, relative to the repo root (or to the unit dir inside `units[]`) */
|
|
14
16
|
file: string
|
|
15
17
|
/** what breaks when it shares a run with other suites */
|
|
16
18
|
why: string
|
|
17
19
|
}
|
|
18
20
|
export type TestsConfig = {
|
|
19
|
-
/** per suite
|
|
21
|
+
/** per suite time in the quick tier (default 2000). A suite whose wall time goes over it under the parallel run is run again alone, and fails when that run burns more CPU than this, or waits more than twice this while the machine is idle. A suite whose CPU inside the parallel run, children included, plus all its worker burned before it stays under it skips that run, unless it may wait on an idle machine or ended with a child not yet reaped. The full tier fails nothing on time */
|
|
20
22
|
budgetMs?: number
|
|
21
23
|
/** suites that break the budget on purpose: the full tier runs them, the quick tier does not */
|
|
22
24
|
slow?: SlowSuite[]
|
|
@@ -25,14 +27,47 @@ export type TestsConfig = {
|
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
export const DEFAULT_BUDGET_MS = 2000
|
|
30
|
+
|
|
31
|
+
/** what a FULL tier run gives each test. Its suites build real repositories
|
|
32
|
+
* and spawn whole gates, and the same file has finished in 6.8s and in 21.3s
|
|
33
|
+
* on identical code depending on what else was running, so bun's 5s default
|
|
34
|
+
* turns machine load into a red release gate.
|
|
35
|
+
*
|
|
36
|
+
* It rides on the COMMAND LINE because nothing else can carry it: a preload
|
|
37
|
+
* calling setDefaultTimeout raises the ceiling for the FIRST file of a run
|
|
38
|
+
* and for nothing after it, which left 106 of this repo's 107 suites at the
|
|
39
|
+
* 5s default while the preload said otherwise. */
|
|
40
|
+
export const FULL_TIMEOUT_MS = 120_000
|
|
41
|
+
/** the wall ceiling of one `bun test` phase. bun's per test timeout never
|
|
42
|
+
* fires on a worker stuck in a SYNCHRONOUS call, and bun's spawnSync has been
|
|
43
|
+
* seen spinning on a child that already exited (planPhases avoids the known
|
|
44
|
+
* trigger), so without this a hang lasts until something outside kills it. The quick
|
|
45
|
+
* tier answers in seconds, so two minutes is far from any real run. */
|
|
46
|
+
export const QUICK_CEILING_MS = 120_000
|
|
47
|
+
/** the full tier runs every suite one after the other, about three minutes */
|
|
48
|
+
export const FULL_CEILING_MS = 1_800_000
|
|
28
49
|
const TESTS_KEYS = ['budgetMs', 'slow', 'serial']
|
|
29
50
|
const SLOW_KEYS = ['file', 'catches', 'whyNotFast']
|
|
30
51
|
const SERIAL_KEYS = ['file', 'why']
|
|
52
|
+
const OWN_FLAGS = ['--budget-ms', '--slow', '--serial', '--declared-in', '--ceiling-ms', '--unit']
|
|
31
53
|
|
|
32
54
|
export const normSuitePath = (file: string): string => posix.normalize(file.replace(/\\/g, '/')).replace(/^\.\//, '')
|
|
33
55
|
|
|
34
56
|
/** the arguments after `bun test`, or null when a shell would interpret the script
|
|
35
57
|
* (cd, env prefix, pipes, quotes, substitution): that body stays the repo's own */
|
|
58
|
+
export type BunfigTest = { root: string | null; ignores: string[]; error: string | null }
|
|
59
|
+
|
|
60
|
+
/** the `[test]` table of a bunfig.toml: where discovery starts, and what it skips */
|
|
61
|
+
export function bunfigTest(text: string | null): BunfigTest {
|
|
62
|
+
if (text == null) return { root: null, ignores: [], error: null }
|
|
63
|
+
let parsed: { test?: { root?: unknown; pathIgnorePatterns?: unknown } }
|
|
64
|
+
try { parsed = Bun.TOML.parse(text) as typeof parsed } catch (e) { return { root: null, ignores: [], error: `bunfig.toml is unparseable (${String(e).slice(0, 120)})` } }
|
|
65
|
+
const t = parsed.test ?? {}
|
|
66
|
+
const raw = t.pathIgnorePatterns
|
|
67
|
+
const ignores = typeof raw === 'string' ? [raw] : Array.isArray(raw) ? raw.filter((x): x is string => typeof x === 'string') : []
|
|
68
|
+
return { root: typeof t.root === 'string' ? normSuitePath(t.root) : null, ignores, error: null }
|
|
69
|
+
}
|
|
70
|
+
|
|
36
71
|
export function parseBunTestScript(script: string): string[] | null {
|
|
37
72
|
const s = script.trim()
|
|
38
73
|
if (!/^bun test(\s|$)/.test(s)) return null
|
|
@@ -54,38 +89,78 @@ export function suiteFlags(t: TestsConfig | undefined): string[] {
|
|
|
54
89
|
|
|
55
90
|
/** the inverse of suiteFlags, plus where the declarations live (printed in
|
|
56
91
|
* every fix line) and the `bun test` arguments after `--` */
|
|
57
|
-
export function parseSuiteFlags(argv: string[]): { tests: TestsConfig; rawArgs: string[]; declaredIn: string } | string {
|
|
92
|
+
export function parseSuiteFlags(argv: string[]): { tests: TestsConfig; rawArgs: string[]; declaredIn: string; ceilingMs: number | null; unit: string } | string {
|
|
58
93
|
const tests: Required<TestsConfig> = { budgetMs: DEFAULT_BUDGET_MS, slow: [], serial: [] }
|
|
59
94
|
let declaredIn = 'repo.config.ts tests'
|
|
95
|
+
let ceilingMs: number | null = null
|
|
96
|
+
let unit = ''
|
|
97
|
+
// the runner's own flags end at `--` OR at the first token that is not one
|
|
98
|
+
// of them: bun swallows a LEADING `--`, so a repo with no declaration hands
|
|
99
|
+
// over the `bun test` arguments with no separator at all
|
|
60
100
|
let i = 0
|
|
61
|
-
for (; i < argv.length
|
|
101
|
+
for (; i < argv.length; i += 2) {
|
|
62
102
|
const flag = argv[i]!
|
|
103
|
+
if (flag === '--') { i++; break }
|
|
104
|
+
if (!OWN_FLAGS.includes(flag)) break
|
|
63
105
|
const value = argv[i + 1]
|
|
64
106
|
if (value == null || value === '--') return `${flag} needs a value`
|
|
65
|
-
i++
|
|
66
107
|
if (flag === '--budget-ms') tests.budgetMs = Number(value)
|
|
67
108
|
else if (flag === '--slow') tests.slow.push({ file: value, catches: '(declared)', whyNotFast: '(declared)' })
|
|
68
109
|
else if (flag === '--serial') tests.serial.push({ file: value, why: '(declared)' })
|
|
69
|
-
else if (flag === '--
|
|
70
|
-
else
|
|
110
|
+
else if (flag === '--ceiling-ms') ceilingMs = Number(value)
|
|
111
|
+
else if (flag === '--unit') unit = value
|
|
112
|
+
else declaredIn = value
|
|
71
113
|
}
|
|
72
114
|
if (!(tests.budgetMs > 0)) return '--budget-ms must be a positive number'
|
|
73
|
-
|
|
115
|
+
if (ceilingMs != null && !(ceilingMs > 0)) return '--ceiling-ms must be a positive number'
|
|
116
|
+
return { tests, rawArgs: argv.slice(i), declaredIn, ceilingMs, unit }
|
|
74
117
|
}
|
|
75
118
|
|
|
76
119
|
/** what `bun test` receives in each phase. The serial phase names its files
|
|
77
|
-
* explicitly, because bun reports no tests when a path is both named and ignored
|
|
78
|
-
|
|
120
|
+
* explicitly, because bun reports no tests when a path is both named and ignored.
|
|
121
|
+
*
|
|
122
|
+
* `noIsolate` (the bun in use knows `--no-isolate`): the parallel phase keeps
|
|
123
|
+
* one global per worker, as a plain `bun test` and the serial phase always
|
|
124
|
+
* did. `--parallel` implies per file isolation, and under it a worker's
|
|
125
|
+
* spawnSync can spin forever on a child that already exited (bun issue
|
|
126
|
+
* 34069), which hung one full run in eight. A repo that passes `--isolate`
|
|
127
|
+
* itself keeps it. */
|
|
128
|
+
export function planPhases(p: { tests: TestsConfig; tier: 'quick' | 'full'; rawArgs: string[]; parallel: boolean; noIsolate?: boolean; configIgnores?: string[]; workers?: number }): { parallel: string[]; serial: string[] | null } {
|
|
79
129
|
const slow = (p.tests.slow ?? []).map((s) => normSuitePath(s.file))
|
|
80
130
|
const serial = (p.tests.serial ?? []).map((s) => normSuitePath(s.file))
|
|
81
|
-
const
|
|
131
|
+
const own = [...new Set([...(p.tier === 'quick' ? slow : []), ...serial])]
|
|
132
|
+
// bun REPLACES the bunfig pathIgnorePatterns with the command line ones, so
|
|
133
|
+
// the repo's own come along whenever this or the repo's arguments name any
|
|
134
|
+
const cliIgnores = p.rawArgs.some((a) => a.startsWith('--path-ignore-patterns'))
|
|
135
|
+
const ignored = own.length > 0 || cliIgnores ? [...own, ...(p.configIgnores ?? []).filter((g) => !own.includes(g))] : []
|
|
82
136
|
const serialRun = serial.filter((f) => p.tier === 'full' || !slow.includes(f))
|
|
137
|
+
// the full tier ceiling, on every file. The quick tier keeps bun's
|
|
138
|
+
// default, where a suite is under a second and a hang must fail fast
|
|
139
|
+
const named = p.rawArgs.some((a) => a === '--timeout' || a.startsWith('--timeout='))
|
|
140
|
+
const args = p.tier === 'full' && !named ? [`--timeout=${FULL_TIMEOUT_MS}`, ...p.rawArgs] : p.rawArgs
|
|
141
|
+
const isolated = p.rawArgs.some((a) => a === '--isolate' || a === '--no-isolate')
|
|
83
142
|
return {
|
|
84
|
-
parallel: [...p.
|
|
85
|
-
serial: serialRun.length > 0 ? [...
|
|
143
|
+
parallel: [...args, ...(p.parallel ? [p.workers != null ? `--parallel=${p.workers}` : '--parallel'] : []), ...(p.parallel && p.noIsolate === true && !isolated ? ['--no-isolate'] : []), ...ignored.map((f) => `--path-ignore-patterns=${f}`)],
|
|
144
|
+
serial: serialRun.length > 0 ? [...args, ...serialRun] : null,
|
|
86
145
|
}
|
|
87
146
|
}
|
|
88
147
|
|
|
148
|
+
/** how many `--parallel` workers one suite run takes. Every test check runs
|
|
149
|
+
* on one slot of the machine pool (core/slots.ts), so a run sized to every
|
|
150
|
+
* core while four other checks do the same puts five times the cores to
|
|
151
|
+
* work. A run takes its own slot's share of the cores plus the share of
|
|
152
|
+
* every slot free when it starts, never more than the cores, and never
|
|
153
|
+
* fewer than half of them: the count is fixed for the whole run, and a gate
|
|
154
|
+
* starts it while its first burst of short checks holds the pool, so the
|
|
155
|
+
* share alone left the longest suite on two workers long after that burst
|
|
156
|
+
* ended. Measured on 8 cores: two workers 12.2s, four 7.6s, eight 7.0s */
|
|
157
|
+
export function poolWorkers(p: { cores: number; size: number; held: number }): number {
|
|
158
|
+
const size = Math.max(1, p.size)
|
|
159
|
+
const mine = Math.max(1, size - Math.max(1, p.held) + 1)
|
|
160
|
+
const floor = Math.max(2, Math.floor(p.cores / 2))
|
|
161
|
+
return Math.max(floor, Math.min(p.cores, Math.round((p.cores * mine) / size)))
|
|
162
|
+
}
|
|
163
|
+
|
|
89
164
|
type Timed = { file: string; ms: number }
|
|
90
165
|
|
|
91
166
|
export function judgeSuites(p: { timings: Record<string, number>; tests: TestsConfig; tier: 'quick' | 'full' }): { over: Timed[]; noLongerSlow: Timed[]; slowest: Timed[] } {
|
|
@@ -102,14 +177,137 @@ export function judgeSuites(p: { timings: Record<string, number>; tests: TestsCo
|
|
|
102
177
|
}
|
|
103
178
|
}
|
|
104
179
|
|
|
105
|
-
/**
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
export
|
|
109
|
-
|
|
110
|
-
|
|
180
|
+
/** one suspect's run alone, in its own `bun test` process. cpuMs is user plus
|
|
181
|
+
* system time of that process and every child it reaped, null when the
|
|
182
|
+
* runtime reports none */
|
|
183
|
+
export type AloneRun = { wallMs: number; cpuMs: number | null }
|
|
184
|
+
/** the 1 minute load average against the core count, read before a suspect runs */
|
|
185
|
+
export type MachineLoad = { load1: number; cores: number }
|
|
186
|
+
/** how far past the budget a suite must WAIT on an idle machine before its
|
|
187
|
+
* wall time fails it: a suite that sleeps or polls costs its owner the same
|
|
188
|
+
* as one that computes, and the factor keeps scheduler noise out */
|
|
189
|
+
export const WAIT_FACTOR = 2
|
|
190
|
+
/** how much more CPU the same suite burns on a saturated machine. CPU time is
|
|
191
|
+
* not load free: measured on 8 cores, one suite went from 1.0s of CPU idle to
|
|
192
|
+
* 1.35 to 1.5s with the cores saturated (load 7 to 19), and did not grow past
|
|
193
|
+
* that. So a loaded machine fails a suite past the budget times this */
|
|
194
|
+
export const LOADED_CPU_FACTOR = 1.5
|
|
195
|
+
/** `ms` is the time the verdict used: cpu, or wall for a wait or when no cpu figure exists */
|
|
196
|
+
export type Confirmed = { file: string; ms: number; wallMs: number; cpuMs: number | null; over: 'cpu' | 'wait' | 'wall' | null }
|
|
197
|
+
|
|
198
|
+
/** the verdict on each suspect once it ran alone. A wall time carries the load
|
|
199
|
+
* of every other process on the machine, and another gate can triple it, so
|
|
200
|
+
* the budget judges CPU: what the suite itself burned, which load moves by
|
|
201
|
+
* a bounded factor only (LOADED_CPU_FACTOR). Wall time still fails a suite
|
|
202
|
+
* far over the budget while the machine was idle, because only then is the
|
|
203
|
+
* wait the suite's own */
|
|
204
|
+
export function confirmOver(suspects: Timed[], alone: Record<string, AloneRun>, budgetMs: number, load: MachineLoad): { over: Confirmed[]; cleared: Confirmed[] } {
|
|
205
|
+
const byFile = new Map(Object.entries(alone).map(([f, run]) => [normSuitePath(f), run]))
|
|
206
|
+
const idle = load.load1 < load.cores
|
|
207
|
+
const judged = suspects.map((t): Confirmed => {
|
|
208
|
+
const run = byFile.get(t.file)
|
|
209
|
+
const wallMs = run?.wallMs ?? t.ms
|
|
210
|
+
const cpuMs = run?.cpuMs ?? null
|
|
211
|
+
if (cpuMs == null) return { file: t.file, ms: wallMs, wallMs, cpuMs, over: wallMs > budgetMs ? 'wall' : null }
|
|
212
|
+
if (cpuMs > budgetMs * (idle ? 1 : LOADED_CPU_FACTOR)) return { file: t.file, ms: cpuMs, wallMs, cpuMs, over: 'cpu' }
|
|
213
|
+
if (idle && wallMs > budgetMs * WAIT_FACTOR) return { file: t.file, ms: wallMs, wallMs, cpuMs, over: 'wait' }
|
|
214
|
+
return { file: t.file, ms: cpuMs, wallMs, cpuMs, over: null }
|
|
215
|
+
})
|
|
216
|
+
return { over: judged.filter((c) => c.over != null), cleared: judged.filter((c) => c.over == null) }
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** how the parallel phase's workers hand the runner the CPU of each suite
|
|
220
|
+
* (suite-cpu-preload.ts writes it, readPhaseCpu reads it) */
|
|
221
|
+
export const SUITE_CPU_ENV = 'SHIPKIT_SUITE_CPU'
|
|
222
|
+
|
|
223
|
+
/** one suite's figures from the parallel phase: its own CPU, children
|
|
224
|
+
* included, and all its worker burned before it began (suite-cpu-preload.ts
|
|
225
|
+
* says why the sum bounds a lone run) */
|
|
226
|
+
export type PhaseCpu = { cpuMs: number; priorMs: number }
|
|
227
|
+
|
|
228
|
+
/** what the workers wrote, keyed like the timings: relative to the run's cwd.
|
|
229
|
+
* `ran` is every suite that finished in the phase, `cpu` the ones whose
|
|
230
|
+
* figure can be trusted. A file bun ran twice keeps its larger bound, and
|
|
231
|
+
* none at all once one of its runs could not be trusted */
|
|
232
|
+
export function readPhaseCpu(text: string, cwd: string): { ran: string[]; cpu: Record<string, PhaseCpu> } {
|
|
233
|
+
const ran = new Set<string>()
|
|
234
|
+
const unsure = new Set<string>()
|
|
235
|
+
const cpu: Record<string, PhaseCpu> = {}
|
|
236
|
+
const base = cwd.replace(/\\/g, '/')
|
|
237
|
+
for (const line of text.split('\n')) {
|
|
238
|
+
const [file, ms, prior] = line.split('\t')
|
|
239
|
+
if (file == null || file === '' || ms == null || prior == null) continue
|
|
240
|
+
const rel = normSuitePath(posix.relative(base, file.replace(/\\/g, '/')))
|
|
241
|
+
if (ms === '-' && prior === '-') { ran.add(rel); unsure.add(rel); continue }
|
|
242
|
+
if (!/^\d+$/.test(ms) || !/^\d+$/.test(prior)) continue
|
|
243
|
+
ran.add(rel)
|
|
244
|
+
const next = { cpuMs: Number(ms), priorMs: Number(prior) }
|
|
245
|
+
const had = cpu[rel]
|
|
246
|
+
if (had == null || next.cpuMs + next.priorMs > had.cpuMs + had.priorMs) cpu[rel] = next
|
|
247
|
+
}
|
|
248
|
+
for (const f of unsure) delete cpu[f]
|
|
249
|
+
return { ran: [...ran].sort(), cpu }
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** which suspects still need their alone run. A lone run can only need
|
|
253
|
+
* what the worker computed by the suite's end, its whole CPU before the
|
|
254
|
+
* suite (priorMs) plus what the suite burned, and sharing the machine only
|
|
255
|
+
* raises CPU, so a suspect whose sum is inside the budget is inside it alone
|
|
256
|
+
* too. A suspect with no figure (a child not yet reaped) runs alone. Only the alone run can see a WAIT, so a
|
|
257
|
+
* suite past WAIT_FACTOR times the budget in wall time still gets it while
|
|
258
|
+
* the machine is idle */
|
|
259
|
+
export function aloneRunsNeeded(suspects: Timed[], parallelCpu: Record<string, PhaseCpu>, budgetMs: number, load: MachineLoad): { rerun: Timed[]; cleared: Confirmed[] } {
|
|
260
|
+
const idle = load.load1 < load.cores
|
|
261
|
+
const rerun: Timed[] = []
|
|
262
|
+
const cleared: Confirmed[] = []
|
|
263
|
+
for (const t of suspects) {
|
|
264
|
+
const f = parallelCpu[t.file]
|
|
265
|
+
const mayWait = idle && t.ms > budgetMs * WAIT_FACTOR
|
|
266
|
+
if (f != null && f.cpuMs + f.priorMs <= budgetMs && !mayWait) cleared.push({ file: t.file, ms: f.cpuMs, wallMs: t.ms, cpuMs: f.cpuMs, over: null })
|
|
267
|
+
else rerun.push(t)
|
|
268
|
+
}
|
|
269
|
+
return { rerun, cleared }
|
|
111
270
|
}
|
|
112
271
|
|
|
272
|
+
/** the suites with a failing test, read off bun's report: a file that failed
|
|
273
|
+
* prints its path as a header line, then its `(fail)` lines under it. A
|
|
274
|
+
* passing file prints nothing at all in a piped run, which is why this reads
|
|
275
|
+
* failures only */
|
|
276
|
+
export function failedSuites(output: string): string[] {
|
|
277
|
+
const out = new Set<string>()
|
|
278
|
+
let cur: string | null = null
|
|
279
|
+
for (const line of output.split('\n')) {
|
|
280
|
+
const head = line.match(/^(\S+\.(?:test|spec)\.[cm]?[jt]sx?):$/)
|
|
281
|
+
if (head != null) { cur = normSuitePath(head[1]!); continue }
|
|
282
|
+
// a file that cannot load prints no `(fail)` line, only this banner
|
|
283
|
+
if (cur != null && (/^\(fail\) /.test(line) || line.startsWith('# Unhandled error between tests'))) out.add(cur)
|
|
284
|
+
}
|
|
285
|
+
return [...out]
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** the FIRST failure of each failing suite, read off the same report as
|
|
289
|
+
* failedSuites: its `(fail)` line, timing dropped, with the first `error:`
|
|
290
|
+
* line bun printed before it in that file. What the record keeps so a page
|
|
291
|
+
* can say why a file is red without a rerun */
|
|
292
|
+
export function failLines(output: string): Map<string, string> {
|
|
293
|
+
const out = new Map<string, string>()
|
|
294
|
+
let cur: string | null = null
|
|
295
|
+
let err: string | null = null
|
|
296
|
+
for (const raw of output.split('\n')) {
|
|
297
|
+
const line = raw.trimEnd()
|
|
298
|
+
const head = line.match(/^(\S+\.(?:test|spec)\.[cm]?[jt]sx?):$/)
|
|
299
|
+
if (head != null) { cur = normSuitePath(head[1]!); err = null; continue }
|
|
300
|
+
if (cur == null || out.has(cur)) continue
|
|
301
|
+
if (err == null && /^\s*error: /.test(line)) err = line.trim()
|
|
302
|
+
const fail = line.match(/^\(fail\) (.*?)(?: \[[\d.]+m?s\])?$/)
|
|
303
|
+
if (fail != null) out.set(cur, clip(err == null ? fail[1]! : `${fail[1]!}: ${err}`))
|
|
304
|
+
else if (line.startsWith('# Unhandled error between tests')) out.set(cur, clip(err == null ? 'an unhandled error between tests' : `an unhandled error between tests: ${err}`))
|
|
305
|
+
}
|
|
306
|
+
return out
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const clip = (s: string): string => (s.length > 300 ? `${s.slice(0, 299)}…` : s)
|
|
310
|
+
|
|
113
311
|
/** the suites bun names as a crashed parallel worker */
|
|
114
312
|
export function crashedWorkers(output: string): { file: string; signal: string }[] {
|
|
115
313
|
return [...output.matchAll(/^\s*✗\s+(\S+)\s+\(worker crashed: (SIG[A-Z0-9]+)\)\s*$/gm)].map((m) => ({ file: normSuitePath(m[1]!), signal: m[2]! }))
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// THE DURATIONS A SUITE RUN LEAVES FOR THE NEXT ONE. bun's --timings file does
|
|
2
|
+
// two jobs: the runner reads the durations back for the budget, and bun itself
|
|
3
|
+
// starts the slowest files first when the file already holds durations. A run
|
|
4
|
+
// that only ever reads a file in its own temp dir schedules in discovery order,
|
|
5
|
+
// so one long suite can start last and set the wall time alone.
|
|
6
|
+
//
|
|
7
|
+
// The kept file is shared by every run of the unit, several at once, so bun
|
|
8
|
+
// never writes it directly: each run gets a copy to read and update, and the
|
|
9
|
+
// kept file is replaced whole by a rename. A kept file that is not in bun's own
|
|
10
|
+
// shape seeds nothing, because bun fails the whole phase on one.
|
|
11
|
+
//
|
|
12
|
+
// bun MERGES what it measured into what it read, so a suite deleted since the
|
|
13
|
+
// last run comes back in the file as a timing this run never took. The runner
|
|
14
|
+
// judges and keeps only the suites the phase ran (suite-cli.ts).
|
|
15
|
+
import { copyFileSync, mkdirSync, readFileSync } from 'fs'
|
|
16
|
+
import { dirname, join } from 'path'
|
|
17
|
+
import { atomicWrite } from '../core/locked-write.ts'
|
|
18
|
+
import { shipkitRoot } from '../ledger/store.ts'
|
|
19
|
+
|
|
20
|
+
/** where a unit's durations live, beside everything else the repo remembers */
|
|
21
|
+
export const timingsHome = (root: string, unit: string): string => join(shipkitRoot(root), 'suite-timings', `${unit === '' ? 'root' : unit}.json`)
|
|
22
|
+
|
|
23
|
+
/** the text, when it is a timings document bun wrote */
|
|
24
|
+
const timingsText = (path: string): string | null => {
|
|
25
|
+
try {
|
|
26
|
+
const text = readFileSync(path, 'utf8')
|
|
27
|
+
// bun refuses a timings file without `version: 1`, and a refusal fails
|
|
28
|
+
// the whole phase, so only a file in bun's own shape is handed on
|
|
29
|
+
const parsed = JSON.parse(text) as { version?: unknown; files?: unknown }
|
|
30
|
+
return parsed != null && parsed.version === 1 && typeof parsed.files === 'object' && parsed.files != null && !Array.isArray(parsed.files) ? text : null
|
|
31
|
+
} catch {
|
|
32
|
+
return null
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** copy the kept durations to where this run's bun reads them. false when
|
|
37
|
+
* there are none worth reading */
|
|
38
|
+
export function seedTimings(kept: string, runFile: string): boolean {
|
|
39
|
+
if (timingsText(kept) == null) return false
|
|
40
|
+
try {
|
|
41
|
+
copyFileSync(kept, runFile)
|
|
42
|
+
return true
|
|
43
|
+
} catch {
|
|
44
|
+
return false
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** keep what this run measured. A run that wrote nothing readable keeps the
|
|
49
|
+
* last good file */
|
|
50
|
+
export function keepTimings(runFile: string, kept: string): void {
|
|
51
|
+
const text = timingsText(runFile)
|
|
52
|
+
if (text == null) return
|
|
53
|
+
try {
|
|
54
|
+
mkdirSync(dirname(kept), { recursive: true })
|
|
55
|
+
atomicWrite(kept, text)
|
|
56
|
+
} catch { /* the next run schedules blind, which is slower and never wrong */ }
|
|
57
|
+
}
|
|
58
|
+
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// does this check body run bun's test runner ITSELF? Tests run through the
|
|
2
|
+
// generated test checks only (the floor `test` slot, one `test:<unit>` per
|
|
3
|
+
// declared unit, core/floor-resolve.ts): they run in the unit's dir with its
|
|
4
|
+
// bunfig and preload, parallel and timed against the budget, and SK018 knows
|
|
5
|
+
// which file each one reaches. A hand written check running `bun test` is a
|
|
6
|
+
// second runner nobody plans, times or dedupes. Rule SK019 fails on one.
|
|
7
|
+
//
|
|
8
|
+
// Read from source text, never by importing it: a legacy check script runs its
|
|
9
|
+
// body on import. The reader only looks at code, never at comments, and only at
|
|
10
|
+
// three shapes that EXECUTE something: an argv literal (`['bun', 'test']`), a
|
|
11
|
+
// command string handed to a shell (`sh -c`, `$\`…\``, `execSync(…)`), and a
|
|
12
|
+
// reference to the suite runner module. A string that merely NAMES a command
|
|
13
|
+
// (a message, a script body written into package.json) is data, and passes.
|
|
14
|
+
import { basename } from 'path'
|
|
15
|
+
import { lexSource } from '../ts/lex.ts'
|
|
16
|
+
|
|
17
|
+
/** a `bun run <script>` that runs the test slot's own body */
|
|
18
|
+
const TEST_SCRIPT = /^test(:raw)?$/
|
|
19
|
+
|
|
20
|
+
/** a package.json script body by name, null when there is none */
|
|
21
|
+
export type ScriptReader = (name: string) => string | null
|
|
22
|
+
|
|
23
|
+
/** what an argv runs, when it is bun's test runner: `bun test`, `bun run
|
|
24
|
+
* test:raw`, `bun run test`, `bun run <script>` whose body runs it (when
|
|
25
|
+
* `scripts` can read the body), or a shell command string that does */
|
|
26
|
+
export function argvRunsTests(argv: readonly string[], scripts?: ScriptReader): string | null {
|
|
27
|
+
const [head, a1, a2] = argv
|
|
28
|
+
if (head == null) return null
|
|
29
|
+
const bin = basename(head)
|
|
30
|
+
if (bin === 'bun' || head === 'process.execPath') {
|
|
31
|
+
if (a1 === 'test') return 'bun test'
|
|
32
|
+
if (a1 === 'run' && a2 != null) return runScript(a2, scripts)
|
|
33
|
+
}
|
|
34
|
+
if (argv.some((a) => a.includes('suite-cli'))) return 'the suite runner (suite-cli)'
|
|
35
|
+
if (['sh', 'bash', 'zsh'].includes(bin) && a1 === '-c' && a2 != null) return commandRunsTests(a2, scripts)
|
|
36
|
+
return null
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** `bun run <name>`: the test slot's own script, or one whose body runs the
|
|
40
|
+
* test runner. One level deep, so a script naming itself cannot loop */
|
|
41
|
+
function runScript(name: string, scripts?: ScriptReader): string | null {
|
|
42
|
+
if (TEST_SCRIPT.test(name)) return `bun run ${name}`
|
|
43
|
+
const body = scripts?.(name)
|
|
44
|
+
if (body == null) return null
|
|
45
|
+
const inner = commandRunsTests(body)
|
|
46
|
+
return inner == null ? null : `bun run ${name} (its script runs ${inner})`
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** a shell command string that runs bun's test runner, at its start or after
|
|
50
|
+
* a `&&`, `;` or `||`, env assignments (`TZ=UTC bun test`) allowed before it */
|
|
51
|
+
export function commandRunsTests(cmd: string, scripts?: ScriptReader): string | null {
|
|
52
|
+
for (const m of cmd.matchAll(/(?:^|&&|;|\|\|)\s*(?:\w+=\S*\s+)*bun\s+(test(?=\s|$)|run\s+([\w:.-]+))/g)) {
|
|
53
|
+
if (m[2] == null) return 'bun test'
|
|
54
|
+
const hit = runScript(m[2], scripts)
|
|
55
|
+
if (hit != null) return hit
|
|
56
|
+
}
|
|
57
|
+
return null
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const LIT = '\\0(\\d+)\\0'
|
|
61
|
+
/** an argv literal in a position that EXECUTES it: `cmd: [`, `exec([`,
|
|
62
|
+
* `spawn([`, `Bun.spawn([`. A list of words held as data is not a run */
|
|
63
|
+
const ARGV_RUN = new RegExp(`(?:\\bcmd\\s*:\\s*|\\b(?:exec|execFile|execFileSync|spawn|spawnSync|execa)\\s*\\(\\s*|\\bBun\\.spawn(?:Sync)?\\s*\\(\\s*)\\[\\s*((?:process\\.execPath|${LIT})(?:\\s*,\\s*${LIT})*)`, 'g')
|
|
64
|
+
/** a string handed straight to a shell: `execSync('…')`, `$\`…\``. A
|
|
65
|
+
* template's chunks come as a run, `${…}` between them */
|
|
66
|
+
const SHELL_CALL = new RegExp(`(?:\\b(?:execSync|exec|execaCommand|spawnSync|spawn)\\s*\\(\\s*|\\$)\`?((?:${LIT})(?:\\$\\{[^\\0\`]*?\\}${LIT})*)`, 'g')
|
|
67
|
+
/** the suite runner's module, resolved or imported */
|
|
68
|
+
const SUITE_REF = new RegExp(`(?:\\bresolve\\s*\\(\\s*|\\bimport\\s*\\(\\s*|\\bfrom\\s*)\`?${LIT}`, 'g')
|
|
69
|
+
|
|
70
|
+
const literals = (run: string, strings: string[]): string[] => [...run.matchAll(new RegExp(`process\\.execPath|${LIT}`, 'g'))].map((x) => (x[1] == null ? 'process.execPath' : strings[Number(x[1])]!))
|
|
71
|
+
|
|
72
|
+
/** every way this source runs bun's test runner, one line each. Empty = it
|
|
73
|
+
* never does. `scripts` reads the package.json a `bun run <name>` names */
|
|
74
|
+
export function sourceRunsTests(src: string, scripts?: ScriptReader): string[] {
|
|
75
|
+
const { code, strings } = lexSource(src)
|
|
76
|
+
const found = new Set<string>()
|
|
77
|
+
for (const m of code.matchAll(ARGV_RUN)) {
|
|
78
|
+
const hit = argvRunsTests(literals(m[1]!, strings), scripts)
|
|
79
|
+
if (hit != null) found.add(hit)
|
|
80
|
+
}
|
|
81
|
+
for (const m of code.matchAll(SHELL_CALL)) {
|
|
82
|
+
// an interpolation reads as one word, so `cd ${dir} && bun test` stays a command
|
|
83
|
+
const hit = commandRunsTests(literals(m[1]!, strings).join('X'), scripts)
|
|
84
|
+
if (hit != null) found.add(hit)
|
|
85
|
+
}
|
|
86
|
+
for (const m of code.matchAll(SUITE_REF)) if (/(?:^|\/)suite-cli(?:\.ts)?$/.test(strings[Number(m[1])]!)) found.add('the suite runner (suite-cli)')
|
|
87
|
+
return [...found]
|
|
88
|
+
}
|