mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
test: prove AC5 by running the scanner, not the predicate (#399)
CODE-REVIEW-399-r1 High: the test named after AC5 called installsPythonDeps on string literals and never executed the directory walk it was supposed to protect. The reviewer showed what that costs: restoring the old hardcoded pair of real names and dropping a third workflow with unpinned installs into .github/workflows left all ten checks green — the exact scenario AC5 describes went undetected. The walk is now a function taking the directory, so the test can run it for real: it builds a temporary directory with three files (a pinned installer, a workflow that installs nothing, and a rogue one) and asserts on what the scanner returns. Reverting the walk to a list of two real names now reddens this test, verified by hand. The mutant is sharpened accordingly: it substitutes the two-name list instead of a one-name list. The old form failed on an unrelated assertion about directory size, so it proved nothing about the scan itself — while the two-name form is indistinguishable from correct code on today's tree, which is what makes it the likely regression. User-Visible: no Issue: #399
This commit is contained in:
@@ -753,8 +753,11 @@ const MUTANT_DEFINITIONS = [
|
||||
+ 'unpinned dependencies unnoticed — the shape #399 removed',
|
||||
patches: [{
|
||||
file: 'test/validate-workflow.test.mjs',
|
||||
find: " const workflows = readdirSync(WORKFLOWS).filter((name) => name.endsWith('.yml'));",
|
||||
replace: " const workflows = ['validate.yml'];",
|
||||
// Список из ДВУХ настоящих имён — самая правдоподобная форма регрессии:
|
||||
// на сегодняшнем дереве она неотличима от корректного кода, и мутант с
|
||||
// одним именем ловился бы по посторонней причине (замечание r1).
|
||||
find: " const files = readdirSync(directory).filter((name) => name.endsWith('.yml'));",
|
||||
replace: " const files = ['validate.yml', 'mutation-gate.yml'];",
|
||||
}],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync, readdirSync } from 'node:fs';
|
||||
import { mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
// #336. Воркфлоу — не текст, а контракт, и ломается он молча: висячая
|
||||
@@ -216,47 +218,65 @@ test('гейты диапазона судят от доказанного пр
|
||||
* установка python-пакетов — файл обязан ставить их из файла пинов. */
|
||||
const installsPythonDeps = (workflow) => /(?:^|\s)(?:python -m )?pip\s+install\s/.test(workflow);
|
||||
|
||||
/** Нарушения пин-контракта в КАТАЛОГЕ workflow-файлов (#399).
|
||||
*
|
||||
* Каталог — параметр, а не константа: только так тест может исполнить ту же
|
||||
* функцию на подставном каталоге и доказать, что она ловит третий файл.
|
||||
* Проверка предиката на строковых литералах этого не доказывает — сам обход
|
||||
* при этом не исполняется и может остаться списком имён (замечание r1).
|
||||
*/
|
||||
export function pinViolations(directory) {
|
||||
const files = readdirSync(directory).filter((name) => name.endsWith('.yml'));
|
||||
const problems = [];
|
||||
let installers = 0;
|
||||
for (const file of files) {
|
||||
const workflow = readFileSync(new URL(file, `file://${directory}`), 'utf8');
|
||||
if (!installsPythonDeps(workflow)) continue;
|
||||
installers += 1;
|
||||
if (!/pip install -r tests_backend\/requirements\.txt/.test(workflow)) {
|
||||
problems.push(`${file}: ставит python-зависимости мимо файла пинов`);
|
||||
}
|
||||
if (/pip install pytest /.test(workflow)) {
|
||||
problems.push(`${file}: остался установ без версий`);
|
||||
}
|
||||
}
|
||||
return { problems, installers, scanned: files.length };
|
||||
}
|
||||
|
||||
test('HA-харнесс ставится по точным версиям, а не по воле резолвера (#392, #399)', () => {
|
||||
// Плавающие версии означают, что «зелёный backend» значит разное в разные
|
||||
// дни: по SHA коммита нельзя сказать, чем его проверяли. Ровно так харнесс
|
||||
// полгода тихо проверял интеграцию против февральского Home Assistant.
|
||||
//
|
||||
// Перебирается ВЕСЬ каталог, а не список имён: новый workflow с
|
||||
// неверсионированной установкой обязан краснеть сам, без правки теста.
|
||||
const workflows = readdirSync(WORKFLOWS).filter((name) => name.endsWith('.yml'));
|
||||
assert.ok(workflows.length >= 2, 'каталог workflows обязан читаться');
|
||||
const installers = [];
|
||||
for (const file of workflows) {
|
||||
const workflow = read(file);
|
||||
if (!installsPythonDeps(workflow)) continue;
|
||||
installers.push(file);
|
||||
assert.match(workflow, /pip install -r tests_backend\/requirements\.txt/,
|
||||
`${file}: ставит python-зависимости мимо файла пинов`);
|
||||
assert.equal(/pip install pytest /.test(workflow), false,
|
||||
`${file}: остался установ без версий`);
|
||||
}
|
||||
// Факт выводится проверкой, а не задаётся ей: список нужен для сообщения об
|
||||
// ошибке, а не для решения, кого проверять.
|
||||
assert.ok(installers.length > 0,
|
||||
const { problems, installers, scanned } = pinViolations(WORKFLOWS);
|
||||
assert.deepEqual(problems, []);
|
||||
assert.ok(scanned >= 2, 'каталог workflows обязан читаться');
|
||||
// Факт выводится проверкой, а не задаётся ей.
|
||||
assert.ok(installers > 0,
|
||||
'ни один workflow не ставит python-зависимости — либо каталог прочитан'
|
||||
+ ' неверно, либо бэкенд-гейт исчез; и то и другое стоит увидеть');
|
||||
});
|
||||
|
||||
test('#399 AC5: проверка ловит новый workflow, которого нет ни в каком списке', () => {
|
||||
// Синтетический третий файл: при переборе по именам он бы не попал в
|
||||
// проверку вовсе — именно так гейт и обходили бы, ничего не нарушая.
|
||||
const rogue = [
|
||||
'name: rogue',
|
||||
'jobs:',
|
||||
' backend:',
|
||||
' steps:',
|
||||
' - run: pip install pytest voluptuous homeassistant',
|
||||
].join('\n');
|
||||
assert.equal(installsPythonDeps(rogue), true,
|
||||
'установка python-зависимостей обязана распознаваться по содержимому');
|
||||
assert.equal(/pip install -r tests_backend\/requirements\.txt/.test(rogue), false,
|
||||
'и такой файл обязан провалить проверку пинов');
|
||||
// Обратный случай: файл без установки не должен требовать пинов.
|
||||
const innocent = 'name: docs\njobs:\n build:\n steps:\n - run: npm ci\n';
|
||||
assert.equal(installsPythonDeps(innocent), false);
|
||||
test('#399 AC5: тот же код ловит третий workflow в подставном каталоге', () => {
|
||||
// Доказательство исполнением, а не рассуждением: строится настоящий
|
||||
// каталог из трёх файлов, и вызывается ТА ЖЕ функция. Если обход вернётся
|
||||
// к списку имён, третий файл в него не попадёт и тест покраснеет — что и
|
||||
// отличает эту проверку от прежней, гонявшей предикат на литералах.
|
||||
const directory = mkdtempSync(join(tmpdir(), 'hp-workflows-'));
|
||||
try {
|
||||
writeFileSync(join(directory, 'validate.yml'),
|
||||
'jobs:\n backend:\n steps:\n - run: pip install -r tests_backend/requirements.txt\n');
|
||||
writeFileSync(join(directory, 'docs.yml'),
|
||||
'jobs:\n build:\n steps:\n - run: npm ci\n');
|
||||
writeFileSync(join(directory, 'zz-rogue.yml'),
|
||||
'jobs:\n backend:\n steps:\n - run: pip install pytest voluptuous homeassistant\n');
|
||||
const { problems, installers, scanned } = pinViolations(`${directory}/`);
|
||||
assert.equal(scanned, 3, 'просмотрены все файлы каталога, а не два имени');
|
||||
assert.equal(installers, 2, 'файл без установки python-пакетов не в счёте');
|
||||
assert.deepEqual(problems, [
|
||||
'zz-rogue.yml: ставит python-зависимости мимо файла пинов',
|
||||
'zz-rogue.yml: остался установ без версий',
|
||||
]);
|
||||
} finally {
|
||||
rmSync(directory, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user