mirror of
https://github.com/affaan-m/ECC.git
synced 2026-09-20 16:47:59 +02:00
fix(hooks): keep venv paths intact and check every pytest candidate
Two holes in the resolver this branch added, both found in review.
A virtualenv path may contain spaces. `resolve_pytest` returned one string and
the caller expanded it unquoted, so `/home/me/my env/bin/python -m pytest` split
into `/home/me/my` and `env/bin/python`. The probe that accepted the candidate
was correctly quoted, so the hook reported the venv as usable and then failed to
run anything in it -- rejecting the push for a reason with nothing to do with
the code being pushed. It now builds an argv array and runs `"${PYTEST_CMD[@]}"`.
The resolver's contract is that every candidate is confirmed to be pytest, and
two of them were not. `ECC_PYTEST_CMD` was returned unchecked, so
`ECC_PYTEST_CMD=true` made the hook run `true -q`, exit 0 and report a Python
project verified by nothing. The PATH branch used `command -v pytest`, which
proves only that a file of that name exists. Both now go through `is_pytest`,
which runs `--version` and requires the output to name pytest -- `--version`
alone is not evidence, since `true --version` also exits 0.
A bad `ECC_PYTEST_CMD` fails the push rather than falling through to the next
candidate. An operator who set it asked for that command, and silently running
a different one hides the misconfiguration -- which is the same silent-gate
failure this branch exists to remove, one level along.
Three regression tests cover the three paths: a venv whose directory name
contains a space, an override that is not pytest, and an override that is.
This commit is contained in:
@@ -306,6 +306,98 @@ if (
|
||||
passed++;
|
||||
else failed++;
|
||||
|
||||
function writeExecutable(filePath, body) {
|
||||
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
||||
fs.writeFileSync(filePath, body);
|
||||
fs.chmodSync(filePath, 0o755);
|
||||
}
|
||||
|
||||
// The Python arm of the hook, exercised without a real interpreter: the stubs
|
||||
// record the argv they were handed, which is what the virtualenv-path regression
|
||||
// is actually about.
|
||||
function runHermeticPythonPrePush({
|
||||
venvName = null,
|
||||
pytestCmd = null,
|
||||
overrideVersionLine = null,
|
||||
} = {}) {
|
||||
const tempDir = createTempDir('codex-pre-push-py-');
|
||||
const projectDir = path.join(tempDir, 'project');
|
||||
const callsPath = path.join(tempDir, 'calls.txt');
|
||||
fs.mkdirSync(projectDir);
|
||||
fs.writeFileSync(path.join(projectDir, 'pyproject.toml'), '[project]\nname = "demo"\n');
|
||||
const initialized = spawnSync('git', ['init', '--quiet'], { cwd: projectDir });
|
||||
assert.strictEqual(initialized.status, 0, initialized.stderr?.toString());
|
||||
|
||||
const env = {
|
||||
ECC_SKIP_GIT_HOOKS: '0',
|
||||
ECC_SKIP_PREPUSH: '0',
|
||||
MSYS_NO_PATHCONV: '1',
|
||||
};
|
||||
|
||||
let venvPython = null;
|
||||
if (venvName) {
|
||||
venvPython = path.join(tempDir, venvName, 'bin', 'python');
|
||||
writeExecutable(venvPython, `#!/bin/sh\nprintf '%s\\n' "$0|$*" >> "${toBashPath(callsPath)}"\nexit 0\n`);
|
||||
env.VIRTUAL_ENV = toBashPath(path.join(tempDir, venvName));
|
||||
}
|
||||
|
||||
if (overrideVersionLine !== null) {
|
||||
const stub = path.join(tempDir, 'bin', 'fake-pytest');
|
||||
writeExecutable(stub, `#!/bin/sh\nif [ "$1" = "--version" ]; then printf '%s\\n' '${overrideVersionLine}'; exit 0; fi\nprintf '%s\\n' "$0|$*" >> "${toBashPath(callsPath)}"\nexit 0\n`);
|
||||
env.ECC_PYTEST_CMD = toBashPath(stub);
|
||||
} else if (pytestCmd !== null) {
|
||||
env.ECC_PYTEST_CMD = pytestCmd;
|
||||
}
|
||||
|
||||
const result = runBash(prePushHook, {
|
||||
env,
|
||||
cwd: projectDir,
|
||||
input: Buffer.from('refs/heads/main 1111111111111111111111111111111111111111 refs/heads/main 0000000000000000000000000000000000000000\n'),
|
||||
});
|
||||
const calls = fs.existsSync(callsPath)
|
||||
? fs.readFileSync(callsPath, 'utf8').trim().split(/\r?\n/).filter(Boolean)
|
||||
: [];
|
||||
cleanup(tempDir);
|
||||
return { result, calls, venvPython };
|
||||
}
|
||||
|
||||
if (
|
||||
test('pre-push runs pytest from a virtualenv whose path contains spaces', () => {
|
||||
const { result, calls, venvPython } = runHermeticPythonPrePush({ venvName: 'my venv' });
|
||||
const python = toBashPath(venvPython);
|
||||
assert.strictEqual(result.status, 0, `${result.stdout}\n${result.stderr}`);
|
||||
assert.deepStrictEqual(calls, [
|
||||
`${python}|-c import pytest`,
|
||||
`${python}|-m pytest -q`,
|
||||
], JSON.stringify({ calls, python, stdout: result.stdout, stderr: result.stderr }, null, 2));
|
||||
})
|
||||
)
|
||||
passed++;
|
||||
else failed++;
|
||||
|
||||
if (
|
||||
test('pre-push rejects an ECC_PYTEST_CMD that does not run pytest', () => {
|
||||
const { result, calls } = runHermeticPythonPrePush({ pytestCmd: 'true' });
|
||||
assert.notStrictEqual(result.status, 0, `${result.stdout}\n${result.stderr}`);
|
||||
assert.match(result.stderr, /ECC_PYTEST_CMD is set to 'true', which does not run pytest/);
|
||||
assert.deepStrictEqual(calls, []);
|
||||
assert.doesNotMatch(result.stdout, /Verification checks passed/);
|
||||
})
|
||||
)
|
||||
passed++;
|
||||
else failed++;
|
||||
|
||||
if (
|
||||
test('pre-push runs an ECC_PYTEST_CMD override that identifies itself as pytest', () => {
|
||||
const { result, calls } = runHermeticPythonPrePush({ overrideVersionLine: 'pytest 8.0.0' });
|
||||
assert.strictEqual(result.status, 0, `${result.stdout}\n${result.stderr}`);
|
||||
assert.strictEqual(calls.length, 1, JSON.stringify(calls));
|
||||
assert.match(calls[0], /\|-q$/);
|
||||
})
|
||||
)
|
||||
passed++;
|
||||
else failed++;
|
||||
|
||||
if (
|
||||
test('check-plugin-cache fails when the installed cache is missing manifest-referenced files', () => {
|
||||
const homeDir = createTempDir('codex-plugin-cache-home-');
|
||||
|
||||
Reference in New Issue
Block a user