kin/tests/test_kin_122_regression.py
2026-03-19 21:56:57 +02:00

248 lines
11 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
KIN-122 regression: rebuild-frontend.sh должен запускать npm install
перед npm run build, если package.json изменился (новее node_modules).
"""
import os
import shutil
import stat
import subprocess
import time
from pathlib import Path
import pytest
SCRIPT_PATH = Path(__file__).parent.parent / "scripts" / "rebuild-frontend.sh"
class TestKIN122RebuildFrontendNpmInstall:
"""Структурные тесты: скрипт содержит условный npm install перед npm run build."""
def test_script_exists(self):
assert SCRIPT_PATH.is_file(), f"rebuild-frontend.sh not found at {SCRIPT_PATH}"
def test_script_is_executable(self):
mode = SCRIPT_PATH.stat().st_mode
assert mode & stat.S_IXUSR, "rebuild-frontend.sh должен быть исполняемым"
def test_script_contains_npm_install_conditional(self):
"""Скрипт должен содержать условный блок npm install (KIN-122)."""
content = SCRIPT_PATH.read_text()
assert "npm install" in content, (
"rebuild-frontend.sh должен содержать 'npm install'"
)
def test_script_npm_install_guarded_by_condition(self):
"""npm install должен быть внутри if-блока, а не вызываться безусловно."""
content = SCRIPT_PATH.read_text()
lines = content.splitlines()
# Найти строку с npm install
npm_install_line_idx = next(
(i for i, line in enumerate(lines) if "npm install" in line and "if" not in line),
None,
)
assert npm_install_line_idx is not None, "Строка с 'npm install' не найдена"
# Должен быть if-блок выше
preceding = "\n".join(lines[:npm_install_line_idx])
assert "if" in preceding, (
"npm install должен быть внутри условного блока"
)
def test_script_checks_node_modules_existence(self):
"""Условие должно проверять наличие node_modules."""
content = SCRIPT_PATH.read_text()
assert "node_modules" in content, (
"Скрипт должен проверять наличие node_modules"
)
def test_script_checks_package_json_mtime(self):
"""Условие должно сравнивать mtime package.json с node_modules (флаг -nt)."""
content = SCRIPT_PATH.read_text()
assert "-nt" in content, (
"Скрипт должен использовать '-nt' для сравнения mtime package.json и node_modules"
)
def test_npm_install_before_npm_run_build(self):
"""npm install должен стоять раньше npm run build в скрипте."""
content = SCRIPT_PATH.read_text()
install_pos = content.find("npm install")
build_pos = content.find("npm run build")
assert install_pos != -1, "npm install не найден в скрипте"
assert build_pos != -1, "npm run build не найден в скрипте"
assert install_pos < build_pos, (
"npm install должен стоять раньше npm run build"
)
def test_npm_run_build_always_runs(self):
"""npm run build должен вызываться вне условного блока — всегда выполняется."""
content = SCRIPT_PATH.read_text()
lines = content.splitlines()
# Найти строку с npm run build (не внутри if-блока)
# Ищем строку, которая содержит "npm run build" и не является частью if-условия
build_lines = [
line for line in lines
if "npm run build" in line and line.strip().startswith("npm run build")
]
assert len(build_lines) >= 1, (
"npm run build должен быть безусловным вызовом"
)
# ---------------------------------------------------------------------------
# Функциональные тесты: реальный запуск скрипта с mock npm
# Покрывают acceptance criteria KIN-122:
# 1. npm install срабатывает автоматически если package.json изменился
# 2. npm install НЕ запускается если package.json не менялся
# ---------------------------------------------------------------------------
@pytest.fixture()
def fake_frontend_env(tmp_path):
"""
Создаёт временную структуру директорий, копирует скрипт туда,
подкладывает mock npm который пишет свои вызовы в лог-файл.
Структура:
tmp_path/
scripts/rebuild-frontend.sh ← копия скрипта
web/frontend/ ← FRONTEND_DIR
bin/npm ← mock npm (логирует вызовы)
npm_calls.log ← лог вызовов (создаётся mock npm)
"""
scripts_dir = tmp_path / "scripts"
scripts_dir.mkdir()
frontend_dir = tmp_path / "web" / "frontend"
frontend_dir.mkdir(parents=True)
(frontend_dir / "package.json").write_text('{"name": "test-kin-122", "scripts": {"build": "echo build"}}')
# Копируем скрипт в temp-окружение
script_copy = scripts_dir / "rebuild-frontend.sh"
shutil.copy(SCRIPT_PATH, script_copy)
script_copy.chmod(0o755)
# Mock npm: записывает все свои аргументы в лог-файл
calls_log = tmp_path / "npm_calls.log"
bin_dir = tmp_path / "bin"
bin_dir.mkdir()
mock_npm = bin_dir / "npm"
mock_npm.write_text(
f'#!/bin/bash\necho "$@" >> "{calls_log}"\n'
)
mock_npm.chmod(0o755)
return {
"script": script_copy,
"frontend_dir": frontend_dir,
"bin_dir": bin_dir,
"calls_log": calls_log,
}
def _run_script(env_data):
"""Запускает rebuild-frontend.sh с mock npm в PATH."""
result = subprocess.run(
[str(env_data["script"])],
env={**os.environ, "PATH": f"{env_data['bin_dir']}:{os.environ.get('PATH', '')}"},
capture_output=True,
text=True,
)
return result
def _get_npm_calls(env_data):
"""Возвращает список аргументов из всех вызовов mock npm."""
log = env_data["calls_log"]
if not log.exists():
return []
return log.read_text().strip().splitlines()
class TestKIN122NpmInstallBehaviorFunctional:
"""
Функциональные тесты: реальный запуск скрипта в изолированном окружении.
Acceptance criteria: хук срабатывает автоматически без ручного вмешательства,
не замедляет pipeline если package.json не менялся.
"""
def test_npm_install_runs_when_node_modules_missing(self, fake_frontend_env):
"""AC-1: npm install вызывается автоматически если node_modules отсутствует."""
# node_modules не создан — условие ! -d node_modules истинно
_run_script(fake_frontend_env)
calls = _get_npm_calls(fake_frontend_env)
assert any("install" in c for c in calls), (
f"npm install должен вызваться при отсутствии node_modules. Вызовы: {calls}"
)
def test_npm_install_runs_when_package_json_newer_than_node_modules(self, fake_frontend_env):
"""AC-1: npm install вызывается если package.json новее node_modules (изменились зависимости)."""
frontend = fake_frontend_env["frontend_dir"]
# Создаём node_modules со старым mtime
node_modules = frontend / "node_modules"
node_modules.mkdir()
old_time = time.time() - 200
os.utime(node_modules, (old_time, old_time))
# package.json уже имеет текущий mtime — он новее node_modules
_run_script(fake_frontend_env)
calls = _get_npm_calls(fake_frontend_env)
assert any("install" in c for c in calls), (
f"npm install должен вызваться если package.json новее node_modules. Вызовы: {calls}"
)
def test_npm_install_skipped_when_node_modules_up_to_date(self, fake_frontend_env):
"""AC-2: npm install НЕ вызывается если node_modules актуален (package.json не менялся)."""
frontend = fake_frontend_env["frontend_dir"]
# Создаём node_modules, затем делаем package.json старше
node_modules = frontend / "node_modules"
node_modules.mkdir()
# node_modules имеет текущее время, package.json — старое
old_time = time.time() - 200
package_json = frontend / "package.json"
os.utime(package_json, (old_time, old_time))
_run_script(fake_frontend_env)
calls = _get_npm_calls(fake_frontend_env)
assert not any("install" in c for c in calls), (
f"npm install НЕ должен вызываться если node_modules актуален. Вызовы: {calls}"
)
def test_npm_run_build_always_runs_regardless_of_install(self, fake_frontend_env):
"""AC-2: npm run build всегда вызывается — pipeline не блокируется если npm install пропущен."""
frontend = fake_frontend_env["frontend_dir"]
# Сценарий: node_modules актуален, npm install не нужен
node_modules = frontend / "node_modules"
node_modules.mkdir()
old_time = time.time() - 200
os.utime(frontend / "package.json", (old_time, old_time))
_run_script(fake_frontend_env)
calls = _get_npm_calls(fake_frontend_env)
assert any("run" in c and "build" in c for c in calls), (
f"npm run build должен вызываться всегда, даже без npm install. Вызовы: {calls}"
)
def test_npm_run_build_runs_after_npm_install(self, fake_frontend_env):
"""AC-1: npm run build запускается после npm install (оба вызова в правильном порядке)."""
# node_modules отсутствует — сработают оба: npm install и npm run build
_run_script(fake_frontend_env)
calls = _get_npm_calls(fake_frontend_env)
install_calls = [c for c in calls if "install" in c]
build_calls = [c for c in calls if "run" in c and "build" in c]
assert install_calls, f"npm install не найден в вызовах: {calls}"
assert build_calls, f"npm run build не найден в вызовах: {calls}"
install_idx = calls.index(install_calls[0])
build_idx = calls.index(build_calls[0])
assert install_idx < build_idx, (
f"npm install (pos {install_idx}) должен стоять перед npm run build (pos {build_idx})"
)