diff --git a/.coveragerc.ci b/.coveragerc.ci new file mode 100644 index 00000000..acc4a253 --- /dev/null +++ b/.coveragerc.ci @@ -0,0 +1,16 @@ +# Coverage config for CI only — used via COVERAGE_RCFILE=.coveragerc.ci. +# +# Why a separate file: the local .coveragerc sets +# `dynamic_context = test_function`, which coverage.py implements via +# sys.settrace. pytest-cov refuses to run that under pytest-xdist +# (DistCovError, pytest-cov#604) and it adds ~+13.6% runtime +# (AGENT_DIARY 2026-09-15). CI only needs the coverage % gate, not +# per-test contexts, so it uses this context-free config and runs in +# parallel (-n auto). Local/bootstrap runs keep the context-rich +# .coveragerc. +[run] +source = src +branch = true + +[report] +show_missing = true diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b286208e..bbf57e17 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -76,7 +76,15 @@ jobs: pip-audit -r requirements-lock.txt --no-deps --disable-pip - name: Full test suite (no filters) - run: python -m pytest tests/ -q --tb=short -m 'not slow and not benchmark' --cov=src --cov-report=term-missing --cov-fail-under=38 + # -n auto (pytest-xdist): run tests on all runner cores. Measured + # locally 197s -> 71s (x2.76, 12 cores); CI runners have 4 cores. + # COVERAGE_RCFILE=.coveragerc.ci: the local .coveragerc sets + # dynamic_context=test_function, which pytest-cov rejects under xdist + # (DistCovError, pytest-cov#604) and which costs ~+13.6% (sys.settrace). + # CI only needs the coverage % gate, so it uses the context-free config. + env: + COVERAGE_RCFILE: .coveragerc.ci + run: python -m pytest tests/ -q --tb=short -m 'not slow and not benchmark' -n auto --cov=src --cov-report=term-missing --cov-fail-under=38 timeout-minutes: 20 # Grader mutation probe (evalmut-перенос, 2026-08-14): мутационный аудит diff --git a/.gitignore b/.gitignore index 4747e308..4afbfa48 100644 --- a/.gitignore +++ b/.gitignore @@ -68,6 +68,7 @@ desktop.ini .coverage .coverage* .coveragerc +!.coveragerc.ci htmlcov/ .tox/ diff --git a/pyproject.toml b/pyproject.toml index 2ac31ab4..a2fae2a2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -101,6 +101,7 @@ dependencies = [ dev = [ "pytest>=7.4.0", "pytest-asyncio>=0.21.0", + "pytest-xdist>=3.5.0", # CI parallel run (-n auto): 197s -> 71s locally "pytest-cov>=7.1.0", # CI coverage reporting (проверено: 7.1.0 + pytest 9.0.3) # Pinned: ruff 0.16+ ужесточил I001 (сортировка вложенных импорт-блоков) — # CI падал на lint 2026-08-05 (10 ошибок в 8 файлах). Бамп — только после diff --git a/tests/test_temporal_facts_generator.py b/tests/test_temporal_facts_generator.py index b6147d7b..39fd514e 100644 --- a/tests/test_temporal_facts_generator.py +++ b/tests/test_temporal_facts_generator.py @@ -7,6 +7,8 @@ import importlib.util from pathlib import Path +import pytest + ROOT = Path(__file__).resolve().parent.parent _spec = importlib.util.spec_from_file_location( @@ -15,8 +17,19 @@ _spec.loader.exec_module(_mod) # noqa: E402 -def test_generate_structure_and_kinds(): - facts = _mod.generate(seed=7) +@pytest.fixture(scope="module") +def facts(): + """generate(seed=7) — один раз на модуль (было 5 вызовов, стало 3). + + generate() делает десятки git-подпроцессов (~7с на вызов). Три теста + используют один и тот же детерминированный набор. test_deterministic_ + generation намеренно вызывает generate дважды (проверка детерминизма), + поэтому его не кэшируем. + """ + return _mod.generate(seed=7) + + +def test_generate_structure_and_kinds(facts): assert len(facts) >= 30 kinds = {f["kind"] for f in facts} assert {"removed", "real", "absent"} <= kinds @@ -27,9 +40,9 @@ def test_generate_structure_and_kinds(): assert f["id"].startswith("T") -def test_removed_ground_truth_validated_via_git(): +def test_removed_ground_truth_validated_via_git(facts): """removed-факт: символ реально был на C~1 (родителе коммита удаления), false@HEAD.""" - removed = [f for f in _mod.generate(seed=7) if f["kind"] == "removed"] + removed = [f for f in facts if f["kind"] == "removed"] assert removed r = removed[0] path = r["support_patterns"][0][5:] @@ -42,8 +55,8 @@ def test_removed_ground_truth_validated_via_git(): assert r["was_true"] is True and r["truth"] is False -def test_absent_names_are_grep_zero(): - for f in _mod.generate(seed=7): +def test_absent_names_are_grep_zero(facts): + for f in facts: if f["kind"] == "absent": assert _mod._grep_zero(f["value"]), f"{f['value']} встречается в src/"