Skip to content

Положить порог покрытия туда, где codecov.yml говорит, что он лежит - #16

Merged
DenisDrobyshev merged 2 commits into
masterfrom
fix/coverage-floor-where-it-says-it-is
Sep 20, 2026
Merged

DenisDrobyshev merged 2 commits into
masterfrom
fix/coverage-floor-where-it-says-it-is

Conversation

@DenisDrobyshev

@DenisDrobyshev DenisDrobyshev commented Sep 20, 2026 •

Copy link
Copy Markdown
Member

codecov.yml объясняет, почему оба статуса Codecov informational:

The real floor lives in pyproject.toml, where pytest --cov enforces it identically on a laptop and in CI.

Обе половины неправда. В pyproject.toml не было никакой конфигурации покрытия, а порог сидел флагом --cov-fail-under=90 в одной команде CI. То есть pytest --cov=glia на ноутбуке не гейтил ничего. Единственная фраза, объясняющая, почему удалённый гейт выключен, — и была той фразой, которая неверна.

Порог

Теперь [tool.coverage.report] fail_under = 90 — ровно там, где сказано. Флаг из CI убран, чтобы число жило в одном месте.

Проверено в обе стороны: при 99 та же команда выходит с 1 и Required test coverage of 99.0% not reached, при 90 — с 0. --cov намеренно не в addopts: второй тестовый job гоняет набор без покрытия специально, и включение отняло бы это.

Числа в README

Оба README говорили «167 offline tests, ~95% coverage». Счёт сегодня верен; покрытие — 94.39%, и главное, что это цифра, которая двигается почти на каждом коммите. Такое проза нести честно не может.

Правило самой организации: опубликованное число измеряется на том прогоне, который оно описывает. Поэтому README теперь называет порог — это гарантия, и она не двигается, — а измеренная цифра остаётся на бейдже, где пересчитывается каждый прогон.

Бейдж сейчас показывает unknown, и причина не в этом репозитории: secrets.CODECOV_TOKEN пуст, fail_ci_if_error правильно сделан условным от него, и загрузка отвергается тихо. То же самое у praxis, mlango и stadion — восемь бейджей на четыре репозитория. Вынесено отдельно, триаж теперь это ловит.

scripts/check_claims.py

Держит оба числа при наборе.

Счёт берётся из собственной коллекции pytest, а не из подсчёта def test_: одна параметризованная функция здесь — четыре собранных теста, так что подсчёт функций дал бы 164 и был бы уверенно неправ.

Порог читается из pyproject.toml регуляркой, привязанной к секции, а не через tomllib: он появился в 3.11, а проект обещает 3.10, и импорт уронил бы скрипт у того, кто пришёл на самом старом обещанном Python.

Пропавшее утверждение сообщается как неверное число. Переписать фразу — это и есть то, от чего такая проверка перестаёт проверять, молча.

Проверено

Семь способов разойтись на копии:

тест добавлен, проза не тронута оба README: says 167 tests, pytest collects 168
счёт уехал в каждом README по отдельности ловится по отдельности
порог поднят в pyproject оба README: says a 90% floor, pyproject enforces 95%
гейт удалён вовсе no [tool.coverage.report] fail_under, so pytest --cov gates at nothing
каждая фраза переписана the sentence was rewritten, so the test count is now unchecked

Регулярка порога прогнана через шесть форм pyproject, включая fail_under в другой секции до и после нужной.

Локально: ruff check чисто, mypy — 28 файлов без замечаний, 167 тестов, покрытие 94.39% против порога 90%.


Первый прогон был красный — и это оказалось полезно

Проверка упала с

could not ask pytest how many tests there are

Причина: CI ставит FORCE_COLOR=1 на каждый шаг, и сводка pytest приходит как \x1b[32m\x1b[32m167 tests collected\x1b[0m…. Шаблон, привязанный к началу строки, до цифр не доходит. На ноутбуке, где цвет никто не форсит, всё сходилось. Лечится --color=no, плюс escape-последовательности теперь снимаются на всякий случай — следующее, что раскрасит вывод pytest, тоже не предупредит.

Вторая ошибка была в сообщении. Вернуть голый None и напечатать одну фразу, не называющую причину, — значит заставить искать одну escape-последовательность ещё одним кругом через CI. Теперь печатается код возврата pytest и последние шесть строк того, что он сказал.

И при написании этого нашлась ошибка хуже той, ради которой писалось. Тестовый файл, который не импортируется, заставляет pytest напечатать 167 tests collected, 1 error и выйти с 2 — а чтение счёта до кода возврата находило 167 и проходило, сообщая, что README точно описывают набор, который невозможно собрать. Код возврата теперь проверяется первым:

  pytest exited 2 and the count is not trustworthy:
      E   ModuleNotFoundError: No module named 'nonexistent_module_xyz'
      ERROR tests/test_zz_broken.py
      !!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!

Прогнано: здоровое дерево с FORCE_COLOR и без, плюс NO_COLOR, TERM=dumb, FORCE_COLOR=3, PY_COLORS=1; неимпортируемый тест; отсутствующий pytest; и добавленный здоровый тест — он по-прежнему даёт 167 против 168 в обоих README.

codecov.yml explains why both Codecov statuses are informational:

    The real floor lives in pyproject.toml, where `pytest --cov` enforces it
    identically on a laptop and in CI.

Neither half was true. pyproject.toml had no coverage configuration at all, and
the floor was a `--cov-fail-under=90` flag on one CI command line, so
`pytest --cov=glia` on a laptop gated at nothing. The one sentence that explains
why the remote gate is switched off was the sentence that was wrong.

The floor is now `[tool.coverage.report] fail_under = 90`, which is what the
sentence claims, and the flag is gone from CI so the number lives in one place.
Checked in both directions: at 99 the same command exits 1 with "Required test
coverage of 99.0% not reached", at 90 it exits 0. `--cov` stays out of addopts
on purpose -- the second test job runs the suite without coverage deliberately,
and forcing it on would take that away.

Both READMEs said "167 offline tests, ~95% coverage". The count is right today;
the coverage is 94.39%, and more to the point it is a figure that moves on
almost every commit, which is not something prose can carry honestly. This
organisation's own rule is that a published number is measured on the run it
describes -- so the README now states the floor, which is a guarantee and does
not move, and the measured figure stays on the badge, where it is recomputed per
run. (That badge currently reads "unknown", for a reason that is not this
repository's: `secrets.CODECOV_TOKEN` is empty, `fail_ci_if_error` is correctly
conditional on it, and the upload is rejected quietly. Reported separately.)

scripts/check_claims.py holds both numbers to the suite. The count comes from
pytest's own collection, not from counting `def test_`: one parametrized
function here is four collected tests, so counting functions would report 164
and be confidently wrong. The floor comes from pyproject.toml, read with a
section-scoped regular expression rather than tomllib, which arrived in 3.11 and
would crash the script on the 3.10 this project still promises.

A claim that has gone missing is reported like a wrong number. Rewriting the
sentence is what makes a check like this stop checking, silently, and that is
the failure worth catching.

Seven ways of drifting were tried against a copy: a test added with the prose
unchanged, each README's count moved on its own, the floor raised in pyproject,
the gate deleted outright, and each sentence rewritten away. All seven reported.
The floor regex was driven through six pyproject shapes, including one with a
`fail_under` in a different section before and after the right one.
…ction

It went red on its first run with

    could not ask pytest how many tests there are

which is true, useless, and my fault twice over.

The cause: CI sets FORCE_COLOR=1 for every step, so pytest's summary arrives as
"\x1b[32m\x1b[32m167 tests collected\x1b[0m…" and a pattern anchored at the
start of the line never reaches the digits. It passes on a laptop, where
nothing forces colour. `--color=no` settles it, and the escapes are stripped
as well -- the next thing to colourise pytest's output will not announce itself
either.

The second fault was the message. Returning a bare None and printing one
sentence that names no cause meant the only way to find a single escape
sequence was another round trip through CI. It now prints pytest's return code
and the last six lines of what it said.

Writing that turned up a worse bug than the one it was written for. A test file
that fails to import makes pytest print "167 tests collected, 1 error" and exit
2 -- and reading the count before the return code found the 167 and passed,
reporting that the READMEs accurately describe a suite that cannot be
collected. The return code is now checked first.

Driven through: a healthy tree with and without FORCE_COLOR, and under NO_COLOR,
TERM=dumb, FORCE_COLOR=3 and PY_COLORS=1; an unimportable test file, which now
exits 2 naming the module; pytest missing entirely; and a healthy added test,
which still reports 167 against 168 in both READMEs.
@DenisDrobyshev
DenisDrobyshev merged commit bdf530e into master Sep 20, 2026
11 checks passed
@DenisDrobyshev
DenisDrobyshev deleted the fix/coverage-floor-where-it-says-it-is branch September 20, 2026 17:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant