researcher: row 98 build packet (#1592)
Packet commit, agents/researcher/work/queue-98 only. Secrets scan clean. Not pushed. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,75 @@
|
|||||||
|
# Row 98 (#1592): Row 77 follow-up, four refusal cases in the service tests
|
||||||
|
|
||||||
|
Owner: Researcher. Reviewer: Filbert. Brief: `docs/plans/2026-10-12_console-pinned-copy.md`, "Row 77
|
||||||
|
follow-up". Built in a scratch worktree at 646e3f77; between 646e3f77 and HEAD nothing changed on the
|
||||||
|
candidate paths or on `scripts/webui-service.sh`. **No change to `scripts/webui-service.sh`**: no new
|
||||||
|
test found a defect in it. Tests only; nothing goes live; no Mos notice.
|
||||||
|
|
||||||
|
## Candidate
|
||||||
|
|
||||||
|
`candidate-manifest.sha256`: two files, repo-relative, hashed from the candidate worktree.
|
||||||
|
`build.patch` is `git diff` of them (71 insertions, 2 deletions; sha256
|
||||||
|
c5127ba725f41bfa8cd255192839597f88803ae3decc53009a5453553c708cc3).
|
||||||
|
|
||||||
|
- `packages/webui/tests/service.test.mjs`: the bus and status cases, and a `busOther` option on the
|
||||||
|
`standIn` systemctl so a second loaded unit can answer `show` apart from the first.
|
||||||
|
- `packages/webui/tests/pinned-copy.test.mjs`: the three rename cases. Placement: the brief names
|
||||||
|
`service.test.mjs`, but the rename cases need the fixture repository (`fixtureRepo`, `run`) that
|
||||||
|
only this file has, beside the existing rename-race test. Sage allowed it (note at rev 841);
|
||||||
|
Filbert confirms.
|
||||||
|
|
||||||
|
## Cases and mutants
|
||||||
|
|
||||||
|
`mutants.py` makes each edit to `scripts/webui-service.sh` alone, runs the test file that holds the
|
||||||
|
case, and restores the script (sha256 checked). Baseline worktree = 3bd3f85c; candidate = this build.
|
||||||
|
|
||||||
|
| # | mutant | case | baseline | candidate |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| 1 | `LoadState` check refuses only `not-found` | N1 (Filbert): `bad-setting`, `error`, `masked` units, each with an `ExecStart` on the copy, refuse naming the state | ALIVE | killed |
|
||||||
|
| 2 | rename failure without a target not refused (the `[ -e "$target" ] \|\| die` line dropped) | N2 (Filbert): stand-in `mv` exits 1 and creates nothing: status 1, stderr "could not rename the copy into <target>", stdout empty, `copies/` empty | ALIVE | killed |
|
||||||
|
| 3 | `mv -T` becomes `mv` | N2 (Filbert), second half: an empty target directory exists at rename time and the real `mv` runs: `copied:` and the target holds the copy | ALIVE | killed |
|
||||||
|
| 4a | status copy id loses both anchors | N1 (Darkwing): `WorkingDirectory` ending `<40 hex>x`, `x<40 hex>` or 41 hex digits prints `copy: none` and no `refactor:` line | ALIVE | killed |
|
||||||
|
| 4b | loses its start anchor | same | ALIVE | killed |
|
||||||
|
| 4c | loses its end anchor | same (the 41-hex case) | ALIVE | killed |
|
||||||
|
| 5a | bus check reads only the first unit | N2 (Darkwing): two loaded units | killed | killed |
|
||||||
|
| 5b | path rule applied to the first unit only | N2 (Darkwing): the second unit runs from the checkout; install refuses naming `[email protected]` and its path, and both `show` calls are logged | ALIVE | killed |
|
||||||
|
|
||||||
|
Baseline 1 of 8 killed; candidate 8 of 8 (`mut-base.txt`, `mut-candidate.txt`). The brief lists five
|
||||||
|
mutants (four edits plus dropping `-T`); 4 is split into three anchor variants and 5 into two, so the
|
||||||
|
table is eight. 5a was already killed in the baseline by the call-log assertion in the existing
|
||||||
|
two-unit case; it is Darkwing's gap (5b) the new case closes.
|
||||||
|
|
||||||
|
## Mutant 3 needs care: what `-T` actually changes
|
||||||
|
|
||||||
|
The brief's reasoning ("without `-T`, `mv` survives for the same reason") held for the stand-in `mv`
|
||||||
|
that ignores its flags. With a real `mv`, a non-empty target does not tell the two apart. The copy's
|
||||||
|
directories are `0555`, and renaming a directory into a new parent needs write permission on the
|
||||||
|
directory itself, so plain `mv` fails with Permission denied, where `mv -T` fails with Directory not
|
||||||
|
empty. Both end at `exists:`. Probed directly, as the user, on a `0555` source:
|
||||||
|
|
||||||
|
| target | `mv -T` | `mv` |
|
||||||
|
|---|---|---|
|
||||||
|
| empty directory | rc 0, replaces it | rc 1, Permission denied, target stays empty |
|
||||||
|
| non-empty directory | rc 1, Directory not empty | rc 1, Permission denied |
|
||||||
|
|
||||||
|
So only an empty target tells them apart: with `-T` the copy replaces it and prints `copied:`;
|
||||||
|
without, `copy` would print `exists:` over an empty directory, a copy that was never written.
|
||||||
|
`a copy that meets an empty target directory ...` pins that. It also fixes in a test the behaviour
|
||||||
|
that an empty directory at the target is replaced. If Filbert judges that incidental, mutant 3 is
|
||||||
|
equivalent under real inputs and that test can go; the non-empty test
|
||||||
|
(`a copy that meets a target holding files ...`) stays either way, since it holds the stand-in-free
|
||||||
|
outcome (`exists:`, nothing moved into the target, temp directory removed).
|
||||||
|
|
||||||
|
## Suites
|
||||||
|
|
||||||
|
- Both service test files on the unmodified script, candidate: 33 pass, 0 fail (32 before the
|
||||||
|
empty-target test).
|
||||||
|
- webui `node --test tests/`: 104 pass, 0 fail.
|
||||||
|
- Not run here: `scripts/test-*.sh` and the control-board suite (tests-only change in webui; Sage's
|
||||||
|
gate rerun covers them).
|
||||||
|
- After the runs no process had a cwd under my scratch (checked through /proc cwd links, 0 found).
|
||||||
|
|
||||||
|
## Secrets scan
|
||||||
|
|
||||||
|
`build.patch`, `mutants.py` and the mutant outputs: no token, secret, password, api key or private
|
||||||
|
key marker.
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
diff --git a/packages/webui/tests/pinned-copy.test.mjs b/packages/webui/tests/pinned-copy.test.mjs
|
||||||
|
index 4103f533..07589efc 100644
|
||||||
|
--- a/packages/webui/tests/pinned-copy.test.mjs
|
||||||
|
+++ b/packages/webui/tests/pinned-copy.test.mjs
|
||||||
|
@@ -169,6 +169,53 @@ test('a copy that loses the rename race leaves no read-only temp directory behin
|
||||||
|
assert.deepEqual(readdirSync(copies), [head], 'the temp directory is removed');
|
||||||
|
});
|
||||||
|
|
||||||
|
+// Row 98 (#1592). The rename's two other outcomes: it fails and no target
|
||||||
|
+// appears (a copy that was never written must not print exists:), it
|
||||||
|
+// meets a target that holds files (mv fails and nothing lands in the
|
||||||
|
+// target), and it meets an empty target directory. Only the last tells mv -T
|
||||||
|
+// from plain mv: the temp copy's directories are 0555, so plain mv cannot move
|
||||||
|
+// it into a target at all (Permission denied) and fails exactly as -T does on a
|
||||||
|
+// full one, but -T may replace an empty directory by rename.
|
||||||
|
+async function copyWithMv(t, mvBody) {
|
||||||
|
+ const { dir, script, head } = fixtureRepo(t);
|
||||||
|
+ const copies = join(dir, 'copies');
|
||||||
|
+ const bin = join(dir, 'bin');
|
||||||
|
+ mkdirSync(bin);
|
||||||
|
+ writeFileSync(join(bin, 'mv'), `#!/bin/sh\n${mvBody}\n`, { mode: 0o755 });
|
||||||
|
+ const env = { PATH: `${bin}:${NODE_DIR}:/usr/bin:/bin`, HOME: dir, XDG_CONFIG_HOME: join(dir, 'config') };
|
||||||
|
+ const child = spawn(script, ['copy', 'refactor', '--copies', copies], { env });
|
||||||
|
+ let stdout = '', stderr = '';
|
||||||
|
+ child.stdout.on('data', (d) => { stdout += d; });
|
||||||
|
+ child.stderr.on('data', (d) => { stderr += d; });
|
||||||
|
+ const [status] = await once(child, 'close');
|
||||||
|
+ return { copies, target: join(copies, head), head, status, stdout, stderr };
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
+test('a copy whose rename fails with no target refuses, writes no copy and leaves no temp directory', async (t) => {
|
||||||
|
+ const r = await copyWithMv(t, 'exit 1');
|
||||||
|
+ assert.equal(r.status, 1);
|
||||||
|
+ assert.equal(r.stderr, `webui-service: could not rename the copy into ${r.target}\n`);
|
||||||
|
+ assert.equal(r.stdout, '');
|
||||||
|
+ assert.deepEqual(readdirSync(r.copies), [], 'no target and no temp directory');
|
||||||
|
+});
|
||||||
|
+
|
||||||
|
+test('a copy that meets a target holding files says exists: and moves nothing into it', async (t) => {
|
||||||
|
+ // The stand-in makes the target appear with a file in it, then runs the real mv with the script's flags.
|
||||||
|
+ const r = await copyWithMv(t, 'for a; do t="$a"; done\nmkdir "$t"\n: > "$t/other"\nexec /usr/bin/mv "$@"');
|
||||||
|
+ assert.equal(r.status, 0, r.stderr);
|
||||||
|
+ assert.equal(r.stdout, `exists: ${r.target}\n`);
|
||||||
|
+ assert.deepEqual(readdirSync(r.copies), [r.head], 'the temp directory is removed');
|
||||||
|
+ assert.deepEqual(readdirSync(r.target), ['other'], 'nothing was moved into the target');
|
||||||
|
+});
|
||||||
|
+
|
||||||
|
+test('a copy that meets an empty target directory replaces it by rename, not by moving into it', async (t) => {
|
||||||
|
+ const r = await copyWithMv(t, 'for a; do t="$a"; done\nmkdir "$t"\nexec /usr/bin/mv "$@"');
|
||||||
|
+ assert.equal(r.status, 0, r.stderr);
|
||||||
|
+ assert.equal(r.stdout, `copied: ${r.target}\ninstall it with: scripts/webui-service.sh install --copy ${r.head}, with the unit's flags\n`);
|
||||||
|
+ assert.deepEqual(readdirSync(r.copies), [r.head], 'the temp directory is gone');
|
||||||
|
+ assert.ok(existsSync(join(r.target, 'packages', 'webui', 'README.md')), 'the copy is the target itself');
|
||||||
|
+});
|
||||||
|
+
|
||||||
|
// serve from a copy: /healthz, the config check, roles and RELEASE from the
|
||||||
|
// copy, and the queue read from --checkout or, without it, from the copy.
|
||||||
|
async function serve(t, cli, args, env) {
|
||||||
|
diff --git a/packages/webui/tests/service.test.mjs b/packages/webui/tests/service.test.mjs
|
||||||
|
index d1226b19..d5d55a1b 100644
|
||||||
|
--- a/packages/webui/tests/service.test.mjs
|
||||||
|
+++ b/packages/webui/tests/service.test.mjs
|
||||||
|
@@ -71,19 +71,22 @@ const BUS_UNITS = '[email protected] loaded active running Mosaic bus host
|
||||||
|
// to stderr. The bus calls install makes are answered apart: `busUnits` for
|
||||||
|
// the mosaic-bus@ list-units, and `bus` for show, by default a bus started
|
||||||
|
// from the run's own copy ($HOME, as run() sets it). `busFails` makes both
|
||||||
|
-// bus calls fail.
|
||||||
|
-function standIn(dir, lines = '', { listFails = false, bus = null, busUnits = BUS_UNITS, busFails = false } = {}) {
|
||||||
|
+// bus calls fail. `busOther` answers show for [email protected]
|
||||||
|
+// apart from `bus`, so a second loaded unit can differ from the first.
|
||||||
|
+function standIn(dir, lines = '', { listFails = false, bus = null, busUnits = BUS_UNITS, busFails = false, busOther = null } = {}) {
|
||||||
|
const bin = join(dir, 'bin');
|
||||||
|
mkdirSync(bin);
|
||||||
|
writeFileSync(join(dir, 'list-units.txt'), lines);
|
||||||
|
writeFileSync(join(dir, 'bus-units.txt'), busUnits);
|
||||||
|
if (bus !== null) writeFileSync(join(dir, 'bus-show.txt'), bus);
|
||||||
|
+ if (busOther !== null) writeFileSync(join(dir, 'bus-show-other.txt'), busOther);
|
||||||
|
const fail = 'echo "Failed to connect to bus" >&2; exit 1';
|
||||||
|
const show = bus !== null ? `cat "${dir}/bus-show.txt"` : `r="$HOME/.local/share/mosaic-console/${COPY_ID}"; printf '%s' "${busShow('$r')}"`;
|
||||||
|
writeFileSync(join(bin, 'systemctl'), `#!/bin/sh
|
||||||
|
echo "$*" >> "${dir}/calls.txt"
|
||||||
|
case "$*" in
|
||||||
|
*list-units*mosaic-bus@*) ${busFails ? fail : `cat "${dir}/bus-units.txt"`} ;;
|
||||||
|
+ *show*[email protected]*) ${busFails ? fail : busOther !== null ? `cat "${dir}/bus-show-other.txt"` : show} ;;
|
||||||
|
*show*mosaic-bus@*) ${busFails ? fail : show} ;;
|
||||||
|
*list-units*) ${listFails ? fail : `cat "${dir}/list-units.txt"`} ;;
|
||||||
|
*is-enabled*) echo enabled ;;
|
||||||
|
@@ -538,10 +541,21 @@ test('install refuses unless the bus unit runs from the same copy, naming both p
|
||||||
|
assert.equal((await install(s.bin)).status, 0);
|
||||||
|
assert.match(readFileSync(join(s.d, 'calls.txt'), 'utf8'), /show mosaic-bus@other\.service -p LoadState -p ExecStart/);
|
||||||
|
rmSync(unitDir, { recursive: true });
|
||||||
|
+ // The second loaded unit is off the copy: refused, naming the second unit and its path.
|
||||||
|
+ const two = `${BUS_UNITS}[email protected] loaded active running Mosaic bus host (other)\n`;
|
||||||
|
+ s = sys({ busUnits: two, bus: busShow(copy), busOther: busShow(REPO) });
|
||||||
|
+ refused(await install(s.bin), `[email protected] runs from ${REPO}/scripts/mosaic, not the copy ${copy}; the bus accepts only its own human-cli.mjs, so every Console bus call would fail human-required until the bus runs from the same copy`);
|
||||||
|
+ assert.match(readFileSync(join(s.d, 'calls.txt'), 'utf8'), /show mosaic-bus@acme\.service -p LoadState -p ExecStart\n.*show mosaic-bus@other\.service -p LoadState -p ExecStart/s);
|
||||||
|
|
||||||
|
// No bus unit installed for the business: refused.
|
||||||
|
s = sys({ bus: 'LoadState=not-found\nExecStart=\n' });
|
||||||
|
refused(await install(s.bin, ['--business', 'acme']), `no bus unit [email protected] is installed (LoadState not-found); the Console on the copy ${copy} needs its bus on that copy`);
|
||||||
|
+ // Only loaded counts: a unit systemd shows at any other LoadState is not the bus, even with an
|
||||||
|
+ // ExecStart on the copy.
|
||||||
|
+ for (const state of ['bad-setting', 'error', 'masked']) {
|
||||||
|
+ s = sys({ bus: busShow(copy).replace('LoadState=loaded', `LoadState=${state}`) });
|
||||||
|
+ refused(await install(s.bin, ['--business', 'acme']), `no bus unit [email protected] is installed (LoadState ${state}); the Console on the copy ${copy} needs its bus on that copy`);
|
||||||
|
+ }
|
||||||
|
// No --business and no mosaic-bus@ unit loaded: refused.
|
||||||
|
s = sys({ busUnits: '' });
|
||||||
|
refused(await install(s.bin), `no mosaic-bus@ unit is loaded, and the Console on the copy ${copy} needs its bus on that copy; name the business with --business ID`);
|
||||||
|
@@ -644,6 +658,14 @@ test('status names the copy and whether refactor has moved past it, and says whe
|
||||||
|
r = await run(dir, ['status', '--dir', unitDir], bin);
|
||||||
|
assert.match(r.stdout, new RegExp(`^copy: none; the unit runs from ${esc(REPO)}$`, 'm'));
|
||||||
|
assert.ok(!/^refactor:/m.test(r.stdout));
|
||||||
|
+ // Only a whole 40-hex last part is a copy: 40 hex plus a character, or 41 hex, is not.
|
||||||
|
+ for (const last of [`${COPY_ID}x`, `x${COPY_ID}`, `${COPY_ID}0`]) {
|
||||||
|
+ unit(join(dir, 'copies', last));
|
||||||
|
+ r = await run(dir, ['status', '--dir', unitDir], bin);
|
||||||
|
+ assert.equal(r.status, 0, r.stderr);
|
||||||
|
+ assert.match(r.stdout, new RegExp(`^copy: none; the unit runs from ${esc(join(dir, 'copies', last))}$`, 'm'), last);
|
||||||
|
+ assert.ok(!/^refactor:/m.test(r.stdout), last);
|
||||||
|
+ }
|
||||||
|
// status reads; it calls nothing that changes the unit.
|
||||||
|
assert.deepEqual(readFileSync(join(dir, readdirSync(dir).find((d) => d.startsWith('si-')), 'calls.txt'), 'utf8').split('\n').filter(Boolean).filter((c) => !/^--user (is-enabled|is-active|show) mosaic-webui\.service( |$)/.test(c)), []);
|
||||||
|
});
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
df0222b622c6dba4242d158fd2cfc8df7afa71bbc68d2a5238f03603b50d6ac5 packages/webui/tests/pinned-copy.test.mjs
|
||||||
|
79dbaf4fee277da2cdfe55b863c267d6dd7df93acbca0adb0e0228ae58bf3196 packages/webui/tests/service.test.mjs
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
worktree /mnt/storage/scratch/researcher/q97-base
|
||||||
|
start 2026-10-12T23:13:23Z
|
||||||
|
1 bus check refuses only not-found: ALIVE (exit 0, 2.9s, service.test.mjs)
|
||||||
|
2 rename failure without a target is not refused: ALIVE (exit 0, 2.1s, pinned-copy.test.mjs)
|
||||||
|
3 mv without -T: ALIVE (exit 0, 2.0s, pinned-copy.test.mjs)
|
||||||
|
4a status copy id loses both anchors: ALIVE (exit 0, 3.0s, service.test.mjs)
|
||||||
|
4b status copy id loses its start anchor: ALIVE (exit 0, 3.5s, service.test.mjs)
|
||||||
|
4c status copy id loses its end anchor: ALIVE (exit 0, 3.3s, service.test.mjs)
|
||||||
|
5a bus check reads only the first unit: killed (exit 1, 3.2s, service.test.mjs)
|
||||||
|
5b bus check applies the path rule to the first unit only: ALIVE (exit 0, 3.2s, service.test.mjs)
|
||||||
|
killed 1 of 8; script restored byte for byte: True
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
worktree /mnt/storage/scratch/researcher/q98
|
||||||
|
start 2026-10-12T23:13:46Z
|
||||||
|
1 bus check refuses only not-found: killed (exit 1, 3.9s, service.test.mjs)
|
||||||
|
2 rename failure without a target is not refused: killed (exit 1, 3.4s, pinned-copy.test.mjs)
|
||||||
|
3 mv without -T: killed (exit 1, 3.2s, pinned-copy.test.mjs)
|
||||||
|
4a status copy id loses both anchors: killed (exit 1, 3.3s, service.test.mjs)
|
||||||
|
4b status copy id loses its start anchor: killed (exit 1, 3.0s, service.test.mjs)
|
||||||
|
4c status copy id loses its end anchor: killed (exit 1, 3.0s, service.test.mjs)
|
||||||
|
5a bus check reads only the first unit: killed (exit 1, 3.1s, service.test.mjs)
|
||||||
|
5b bus check applies the path rule to the first unit only: killed (exit 1, 2.8s, service.test.mjs)
|
||||||
|
killed 8 of 8; script restored byte for byte: True
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# Row 98 (#1592) mutants of scripts/webui-service.sh. Each edit must match exactly once and each runs
|
||||||
|
# alone against the test file that holds its case. Every run starts in its own process group and the
|
||||||
|
# group is killed when the run ends or times out. The script is restored from the bytes read before
|
||||||
|
# the edit, and its sha256 is checked against its start at the end.
|
||||||
|
# Usage: mutants.py <worktree> <out.txt> [timeout-seconds]
|
||||||
|
import hashlib, os, signal, subprocess, sys, time
|
||||||
|
|
||||||
|
WT, OUT = sys.argv[1], sys.argv[2]
|
||||||
|
TIMEOUT = int(sys.argv[3]) if len(sys.argv) > 3 else 300
|
||||||
|
S = 'scripts/webui-service.sh'
|
||||||
|
SVC, PIN = 'tests/service.test.mjs', 'tests/pinned-copy.test.mjs'
|
||||||
|
|
||||||
|
# (name, old, new, test file)
|
||||||
|
M = [
|
||||||
|
('1 bus check refuses only not-found', '[ "$load" = loaded ] ||', '[ "$load" != not-found ] ||', SVC),
|
||||||
|
('2 rename failure without a target is not refused', ' [ -e "$target" ] || die "could not rename the copy into $target"\n', '', PIN),
|
||||||
|
('3 mv without -T', 'mv -T "$COPY_TMP" "$target"', 'mv "$COPY_TMP" "$target"', PIN),
|
||||||
|
('4a status copy id loses both anchors', '[[ "$copy" =~ ^[0-9a-f]{40}$ ]]', '[[ "$copy" =~ [0-9a-f]{40} ]]', SVC),
|
||||||
|
('4b status copy id loses its start anchor', '[[ "$copy" =~ ^[0-9a-f]{40}$ ]]', '[[ "$copy" =~ [0-9a-f]{40}$ ]]', SVC),
|
||||||
|
('4c status copy id loses its end anchor', '[[ "$copy" =~ ^[0-9a-f]{40}$ ]]', '[[ "$copy" =~ ^[0-9a-f]{40} ]]', SVC),
|
||||||
|
('5a bus check reads only the first unit', ' for unit in $units; do\n out="$(systemctl --user show', ' for unit in $(printf \'%s\\n\' "$units" | head -n 1); do\n out="$(systemctl --user show', SVC),
|
||||||
|
('5b bus check applies the path rule to the first unit only', ' for p in $paths; do\n', ' [ "$unit" = "$(printf \'%s\\n\' "$units" | head -n 1)" ] || continue\n for p in $paths; do\n', SVC),
|
||||||
|
]
|
||||||
|
|
||||||
|
env = dict(os.environ, TMPDIR=os.environ.get('TMPDIR', '/mnt/storage/scratch/tmp'))
|
||||||
|
full = os.path.join(WT, S)
|
||||||
|
sha = lambda: hashlib.sha256(open(full, 'rb').read()).hexdigest()
|
||||||
|
start = sha()
|
||||||
|
|
||||||
|
def run(test, limit):
|
||||||
|
t0 = time.time()
|
||||||
|
p = subprocess.Popen(['node', '--test', test], cwd=os.path.join(WT, 'packages/webui'), env=env,
|
||||||
|
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, start_new_session=True, text=True)
|
||||||
|
try:
|
||||||
|
out, _ = p.communicate(timeout=limit)
|
||||||
|
code = p.returncode
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
code = 'timeout'
|
||||||
|
finally:
|
||||||
|
try: os.killpg(p.pid, signal.SIGKILL)
|
||||||
|
except ProcessLookupError: pass
|
||||||
|
try: p.communicate(timeout=5)
|
||||||
|
except Exception: pass
|
||||||
|
return code, round(time.time() - t0, 1)
|
||||||
|
|
||||||
|
with open(OUT, 'w') as f:
|
||||||
|
def say(s): print(s); f.write(s + '\n'); f.flush()
|
||||||
|
say(f'worktree {WT}')
|
||||||
|
say(f'start {time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())}')
|
||||||
|
killed = 0
|
||||||
|
for name, old, new, test in M:
|
||||||
|
orig = open(full, 'rb').read()
|
||||||
|
src = orig.decode()
|
||||||
|
if src.count(old) != 1:
|
||||||
|
say(f'{name}: NO MATCH ({src.count(old)}), skipped'); continue
|
||||||
|
try:
|
||||||
|
open(full, 'w').write(src.replace(old, new))
|
||||||
|
code, secs = run(test, TIMEOUT)
|
||||||
|
finally:
|
||||||
|
open(full, 'wb').write(orig)
|
||||||
|
dead = code not in (0, 'timeout')
|
||||||
|
killed += dead
|
||||||
|
verdict = 'killed' if dead else 'STALLED' if code == 'timeout' else 'ALIVE'
|
||||||
|
say(f'{name}: {verdict} (exit {code}, {secs}s, {test.split("/")[-1]})')
|
||||||
|
ok = sha() == start
|
||||||
|
say(f'killed {killed} of {len(M)}; script restored byte for byte: {ok}')
|
||||||
|
sys.exit(0 if ok else 1)
|
||||||
Reference in New Issue
Block a user