fix(ptica): три места, где код делал не то, что говорил (#2464) #3003

Merged
bot-backend merged 1 commit from fix/2464-three-small into main 2026-08-20 19:18:01 +00:00
Collaborator

Три оставшихся [L]-пункта эпика одним PR — все три одного класса: код делает не то, что сам о себе сообщает.

1. Период водоканала попадал в лог, но не в ответ

result = {"records": len(records), **counts, "period": period}
logger.info("water_reserves[%s] done: %s", system_kind, result)   # ← период здесь есть
return {k: v for k, v in result.items() if isinstance(v, int)}    # ← и здесь его нет

period — строка вида «III кв. 2025» или None, поэтому фильтр выбрасывал его всегда. По логам казалось, что период отдаётся; вызывающий не получал его ни разу.

Фильтр стоял ради аннотации dict[str, int] и ничего не защищал: соседняя ветка load_water_reserves кладёт в тот же словарь {"error": str(...)}, а единственный потребитель — задача sync_water_reserves — результат просто логирует и возвращает.

Период полезен: без него «загружено 42 записи» не отличить от прошлогодних. Аннотация исправлена вместе с фильтром — иначе следующий проход mypy вернул бы его обратно; на это поставлен отдельный тест.

2. TOCTOU в get_sqlite_info

if not p.exists():
    return info
st = p.stat()          # ← вне try/except

try/except покрывал только sqlite3.connect ниже. Файл между проверками реально исчезает — его переписывает выгрузка Объектива, — и OSError улетал наружу, превращая диагностическую функцию в источник отказа. Теперь отдаём то, что успели узнать, с ключом stat_error.

3. Предупреждение placement называло не тот размер

footprint = _centered_footprint(cell.cx, cell.cy, fp_w, fp_d)   # ставим этим
...
logger.warning("… placed %d of %d sections (%.0fx%.0f m) — участок мал",
               , house.footprint_w_m, house.footprint_d_m)      # печатаем это

Если элемент программы переопределил габарит, сообщение называет размер, которым никто не пытался ставить. Диагностика уводит от причины — тот же класс, что «диагностика со знаком вопроса».

Как проверено

  • Двусторонне: против origin/main четыре теста красные с конкретными значениями:
    • период выброшен из ответа: {'records': 1, 'inserted': 1, 'updated': 0};
    • аннотация всё ещё требует только int: dict[str, int];
    • TOCTOU-тест падает с FileNotFoundError из самой функции;
    • в предупреждении найден каталожный размер.
  • Контроли зелёные с обеих сторон: отсутствующий файл по-прежнему даёт exists=False без ошибки; fp_w/fp_d — действительно те переменные, которыми ставят (иначе первый тест сверял бы имена, а не смысл).
  • pytest backend/tests/services/ — 3207 passed, 14 skipped; смежные test_placement / test_program_placement — 44 passed.

Часть эпика #2464.

Три оставшихся [L]-пункта эпика одним PR — все три одного класса: код делает не то, что сам о себе сообщает. ## 1. Период водоканала попадал в лог, но не в ответ ```python result = {"records": len(records), **counts, "period": period} logger.info("water_reserves[%s] done: %s", system_kind, result) # ← период здесь есть return {k: v for k, v in result.items() if isinstance(v, int)} # ← и здесь его нет ``` `period` — строка вида «III кв. 2025» или None, поэтому фильтр выбрасывал его **всегда**. По логам казалось, что период отдаётся; вызывающий не получал его ни разу. Фильтр стоял ради аннотации `dict[str, int]` и **ничего не защищал**: соседняя ветка `load_water_reserves` кладёт в тот же словарь `{"error": str(...)}`, а единственный потребитель — задача `sync_water_reserves` — результат просто логирует и возвращает. Период полезен: без него «загружено 42 записи» не отличить от прошлогодних. Аннотация исправлена вместе с фильтром — иначе следующий проход mypy вернул бы его обратно; на это поставлен отдельный тест. ## 2. TOCTOU в `get_sqlite_info` ```python if not p.exists(): return info st = p.stat() # ← вне try/except ``` `try/except` покрывал только `sqlite3.connect` ниже. Файл между проверками реально исчезает — его переписывает выгрузка Объектива, — и OSError улетал наружу, превращая **диагностическую** функцию в источник отказа. Теперь отдаём то, что успели узнать, с ключом `stat_error`. ## 3. Предупреждение placement называло не тот размер ```python footprint = _centered_footprint(cell.cx, cell.cy, fp_w, fp_d) # ставим этим ... logger.warning("… placed %d of %d sections (%.0fx%.0f m) — участок мал", …, house.footprint_w_m, house.footprint_d_m) # печатаем это ``` Если элемент программы переопределил габарит, сообщение называет размер, **которым никто не пытался ставить**. Диагностика уводит от причины — тот же класс, что «диагностика со знаком вопроса». ## Как проверено - **Двусторонне:** против `origin/main` четыре теста красные с конкретными значениями: - `период выброшен из ответа: {'records': 1, 'inserted': 1, 'updated': 0}`; - `аннотация всё ещё требует только int: dict[str, int]`; - TOCTOU-тест падает с `FileNotFoundError` из самой функции; - в предупреждении найден каталожный размер. - **Контроли зелёные с обеих сторон:** отсутствующий файл по-прежнему даёт `exists=False` без ошибки; `fp_w`/`fp_d` — действительно те переменные, которыми ставят (иначе первый тест сверял бы имена, а не смысл). - `pytest backend/tests/services/` — 3207 passed, 14 skipped; смежные `test_placement` / `test_program_placement` — 44 passed. Часть эпика #2464.
bot-backend added 1 commit 2026-08-20 19:00:03 +00:00
fix(ptica): три места, где код делал не то, что говорил (#2464)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 10s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 10s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 2m17s
CI / backend-tests (pull_request) Successful in 17m20s
f227768a53
1. `load_water_reserves_from_docx` собирал `result` с ключом `period`,
   печатал ПОЛНЫЙ словарь в лог, а возвращал
   `{k: v for … if isinstance(v, int)}` — период это строка или None,
   поэтому выбрасывался всегда. По логам казалось, что период отдаётся;
   вызывающий не получал его ни разу.

   Фильтр стоял ради аннотации `dict[str, int]` и ничего не защищал:
   соседняя ветка `load_water_reserves` кладёт в тот же словарь
   `{"error": str(...)}`, а единственный потребитель — задача
   `sync_water_reserves` — результат логирует и возвращает как есть.
   Период полезен: без него «загружено 42 записи» не отличить от
   прошлогодних. Аннотация исправлена, иначе следующий проход mypy вернул
   бы фильтр обратно — на это поставлен отдельный тест.

2. `get_sqlite_info` — TOCTOU: `p.exists()`, затем незащищённый
   `p.stat()`. try/except покрывал только `sqlite3.connect` ниже, поэтому
   OSError из stat улетал наружу и превращал диагностическую функцию в
   источник отказа. Файл между проверками реально исчезает — его
   переписывает выгрузка Объектива. Теперь отдаём то, что успели узнать,
   с ключом `stat_error`.

3. `place_program` — предупреждение «участок мал» печатало КАТАЛОЖНЫЕ
   `house.footprint_*`, хотя ставили по `fp_w`/`fp_d`. При
   переопределённом в программе габарите сообщение называло размер,
   которым никто не пытался ставить, и уводило от причины.

Двусторонне: против origin/main четыре теста красные с конкретными
значениями («период выброшен из ответа: {'records': 1, 'inserted': 1,
'updated': 0}», «аннотация всё ещё требует только int: dict[str, int]»).

Контроли зелёные с обеих сторон: отсутствующий файл по-прежнему даёт
exists=False без ошибки; `fp_w`/`fp_d` — действительно те размеры,
которыми ставят (иначе первый тест сверял бы имена, а не смысл).

pytest backend/tests/services/ — 3207 passed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
bot-backend merged commit 5cd3502493 into main 2026-08-20 19:18:01 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#3003
No description provided.