Repository navigation
Expand file tree
/
Copy pathtest-once.mjs
More file actions
211 lines (184 loc) · 6.43 KB
/
Copy pathtest-once.mjs
File metadata and controls
211 lines (184 loc) · 6.43 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
#!/usr/bin/env node
/**
* Runs the full jsdom suite (`pnpm test`) unless this exact tree is the last
* one that passed it in this worktree. The pre-push hook calls it with
* `--pre-push`, which also requires every pushed ref to point at that tree;
* otherwise it runs the suite on the working tree, as `pnpm test` did.
*
* A run is reused by content only, never by which files changed: the same git
* tree, Node version and architecture, installed dependencies and
* `KEYED_ENV`, on a working tree with no changes and no untracked files, where
* no tracked file was written while the suite ran. Ignored files are outside
* the key, since a push never carries them. Whatever it cannot establish, it
* runs the suite for.
*/
import { execFileSync, spawnSync } from 'node:child_process';
import { createHash } from 'node:crypto';
import { lstatSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { isatty } from 'node:tty';
import { fileURLToPath } from 'node:url';
const root = resolve(import.meta.dirname, '..');
// Environment variables that change what the suite checks, through Vitest
// (CI, NODE_ENV), the runtime (locale, time zone, NODE_OPTIONS) or a test
// (UPDATE_DEFAULTS, UPDATE_TASTY_CONFIG). A test that starts reading another
// one adds it here.
// Vitest also enters CI mode on a provider's variable (JENKINS_URL, …), which
// only rejects a committed `.only`; that is left to CI.
const KEYED_ENV = [
'CI',
'LANG',
'LC_ALL',
'LC_MESSAGES',
'NODE_ENV',
'NODE_OPTIONS',
'TZ',
'UPDATE_DEFAULTS',
'UPDATE_TASTY_CONFIG',
];
const DELETED = /^0+$/;
// `<local ref> <local sha> <remote ref> <remote sha>`, read from the end: the
// local ref is the source as typed, which can contain spaces.
const PUSH_LINE = / ([0-9a-f]{40,64}) \S+ [0-9a-f]{40,64}$/;
// `git ls-files -v` tags: lowercase for assume-unchanged, S for skip-worktree.
// Edits to those files never show in `git status`.
const HIDDEN = /^[a-zS] /m;
/** The local shas a push sends, from git's pre-push input; a deletion sends none. */
export function parsePushedShas(input) {
return input
.split('\n')
.filter((line) => line !== '')
.map((line) => {
const sha = PUSH_LINE.exec(line)?.[1];
if (!sha) throw new Error(`Unexpected pre-push line: ${line}`);
return sha;
})
.filter((sha) => !DELETED.test(sha));
}
function git(cwd, ...args) {
return execFileSync('git', args, {
cwd,
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'ignore'],
}).trim();
}
/** Whether the working tree is clean, its tree, and what else a run depends on. */
function readState(cwd) {
// Outside the top of its own repository, git would report an enclosing one.
if (git(cwd, 'rev-parse', '--show-prefix') !== '') {
throw new Error(`${cwd} is not the top of a work tree`);
}
const installed = readFileSync(resolve(cwd, 'node_modules/.pnpm/lock.yaml'));
const installHash = createHash('sha256').update(installed).digest('hex');
const status = git(cwd, 'status', '--porcelain', '--untracked-files=normal');
return {
clean: status === '' && !HIDDEN.test(git(cwd, 'ls-files', '-v')),
tree: git(cwd, 'rev-parse', 'HEAD^{tree}'),
key: JSON.stringify([
process.version,
process.arch,
...KEYED_ENV.map((name) => process.env[name]),
installHash,
]),
};
}
/** What a passed run of `state` is recorded as; a tree with changes has none. */
function runId(state) {
return state?.clean ? `${state.tree} ${state.key}` : null;
}
/**
* Writes a marker and returns its mtime: the run's start on the clock the
* filesystem stamps files with, which can trail `Date.now()` (Linux).
*/
function markStart(cwd) {
const marker = `${stampPath(cwd)}-start`;
writeFileSync(marker, '');
return lstatSync(marker).mtimeMs;
}
/** Whether no tracked file was written since `time`, even with its content restored. */
function untouchedSince(cwd, time) {
return git(cwd, 'ls-files', '-z')
.split('\0')
.filter(Boolean)
.every((file) => lstatSync(resolve(cwd, file)).mtimeMs < time);
}
/** Per worktree, inside the git directory, so it never shows up as a change. */
function stampPath(cwd) {
return resolve(cwd, git(cwd, 'rev-parse', '--git-path', 'test-once'));
}
function readPushedTrees(input, cwd) {
return parsePushedShas(input).map((sha) =>
git(cwd, 'rev-parse', `${sha}^{tree}`),
);
}
function attempt(fn) {
try {
return fn();
} catch {
return null;
}
}
function runSuite(cwd) {
// A hook in a linked worktree exports GIT_DIR, which would point any git
// command a test runs elsewhere at this repository.
const env = Object.fromEntries(
Object.entries(process.env).filter(([name]) => !name.startsWith('GIT_')),
);
const result = spawnSync('pnpm', ['test'], {
cwd,
env,
stdio: ['ignore', 'inherit', 'inherit'],
});
if (result.error) {
console.error(
`test-once: could not run pnpm test: ${result.error.message}`,
);
}
return result.status ?? 1;
}
function readInput() {
// `process.stdin.isTTY` would open fd 0 as non-blocking, and a read before
// git has written its lines would then fail.
return isatty(0) ? '' : readFileSync(0, 'utf8');
}
export function main(
args,
{ cwd = root, run = runSuite, input = readInput } = {},
) {
const before = attempt(() => readState(cwd));
const pushedTrees = args.includes('--pre-push')
? attempt(() => readPushedTrees(input(), cwd))
: [];
const stamp = attempt(() => readFileSync(stampPath(cwd), 'utf8'));
const id = runId(before);
if (
id &&
stamp === id &&
pushedTrees?.every((tree) => tree === before.tree)
) {
console.log(
`test-once: the full jsdom suite already passed on tree ${before.tree.slice(0, 8)}; skipping it.`,
);
return 0;
}
const started = id ? attempt(() => markStart(cwd)) : null;
const status = run(cwd);
// An edit, even a reverted one, or an install during the run leaves a
// result for a state no one has.
if (
status === 0 &&
started &&
runId(attempt(() => readState(cwd))) === id &&
attempt(() => untouchedSince(cwd, started))
) {
attempt(() => writeFileSync(stampPath(cwd), id));
}
return status;
}
// Node can keep a symlinked path in either one, and a mismatch would exit 0
// without running anything, so compare real paths.
if (
attempt(() => realpathSync(process.argv[1])) ===
realpathSync(fileURLToPath(import.meta.url))
)
process.exit(main(process.argv.slice(2)));