Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 19 additions & 10 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,13 @@ jobs:
- suite: md-heavy
label: 'Unit Tests md-heavy'
marker: 'md_heavy'
# coverage נמדד בגרסה אחת בלבד, כדי לא לשלם פעמיים על ההאטה שהוא מוסיף לטסטים.
# 3.11, כי זו הגרסה שהפרודקשן רץ עליה (Dockerfile). הרשומה מוסיפה את
# ``coverage: true`` לכל המסלולים של הגרסה הזו: רשומת include מתווספת לכל צירוף
# שהיא לא דורסת בו ערך מקורי של המטריצה
# (``jobs.<job_id>.strategy.matrix.include`` בתיעוד של GitHub Actions).
- python-version: '3.11'
coverage: true
services:
# המסלול ``md-heavy`` אינו צריך מסד: ``image`` ריק אומר ל-runner לא להרים את
# השירות (``jobs.<job_id>.services.<service_id>.image`` בתיעוד של GitHub
Expand Down Expand Up @@ -283,27 +290,29 @@ jobs:
GITHUB_TOKEN: ${{ secrets.TEST_GITHUB_TOKEN }}
PASTEBIN_API_KEY: ${{ secrets.TEST_PASTEBIN_KEY }}
FEATURE_MY_COLLECTIONS: '1'
# Minimize retry/backoff delays during tests to avoid long waits
HTTP_RESILIENCE_MAX_ATTEMPTS: '1'
HTTP_RESILIENCE_BACKOFF_BASE: '0'
HTTP_RESILIENCE_BACKOFF_MAX: '0'
HTTP_RESILIENCE_JITTER: '0'
REQUESTS_RETRIES: '0'
REQUESTS_RETRY_BACKOFF: '0'
AIOHTTP_TIMEOUT_TOTAL: '6'
# ערכי הבדיקה של ההמתנות שקוד הייצור עושה בכוונה — rate limit, retry, backoff —
# נקבעים ב-tests/conftest.py ולא כאן, כדי שיחולו על כל הרצה של הטסטים: גם
# deploy.yml וגם הרצה מקומית. ראו docs/testing.rst.
# ``-m`` בוחר את הטסטים של המסלול (``matrix.marker``). מסלול שלא נבחר בו אף
# טסט נכשל ולא עובר בשקט: pytest יוצא בקוד 5 כשלא נאסף אף טסט, גם כשכולם
# סוננו ב-``-m`` (נמדד ב-pytest 8.4.2, עם ``-n`` ובלעדיו). כך שם סימון ששונה
# כאן ולא בטסטים, או בכל הטסטים ולא כאן, מפיל את המסלול ``md-heavy``. טסט בודד
# שהסימון שלו חסר או שגוי פשוט רץ במסלול הרגיל. את קוד היציאה, ואת זה ששני
# הביטויים משלימים זה את זה, מקבע ``tests/test_required_checks_are_listed.py``.
#
# דגלי ה-coverage עוברים רק במסלולים ש-``matrix.coverage`` דלוק בהם. בשאר המסלולים
# המאפיין לא מוגדר, ומאפיין שלא קיים מחזיר מחרוזת ריקה (contexts בתיעוד של GitHub
# Actions); ``&&`` ו-``||`` מחזירים את הערך עצמו ולא true/false (``And.cs`` ו-``Or.cs``
# ב-actions/runner), ולכן שם הביטוי ריק. אותו טסט בודק שאין ``--cov`` מחוץ לביטוי.
run: |
pytest -n auto --dist=loadscope -v -o addopts="" -m "${{ matrix.marker }}" \
--cov=. --cov-report=xml --cov-report=term \
${{ matrix.coverage && '--cov=. --cov-report=xml --cov-report=term' || '' }} \
--durations=0 --json-report --json-report-file=unit-durations.json

- name: Upload coverage to Codecov
if: always()
# רק במסלולים שמדדו coverage. Codecov ממזג את ההעלאות של אותו קומיט, ובלי
# ``after_n_builds`` ב-codecov.yml הוא לא מחכה למספר מסוים שלהן.
if: ${{ always() && matrix.coverage }}
uses: codecov/codecov-action@v4
with:
files: coverage.xml
Expand Down
2 changes: 1 addition & 1 deletion docs/ci-cd.rst
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ CI/CD Guide
--------------------

- **Code Quality & Security** – בדיקות סטטיות ואבטחה
- **Unit Tests (3.11/3.12)** ו-**Unit Tests md-heavy (3.11/3.12)** – טסטי היחידה, בשני מסלולים לכל גרסת פייתון שרצים במקביל. ``Unit Tests`` מריץ את כל הטסטים חוץ מאלה שמסומנים ``md_heavy``, ו-``Unit Tests md-heavy`` מריץ רק אותם: טסטי פרסר ה-Markdown הכבדים (ראו :doc:`testing`). כל מסלול מדווח סטטוס חובה משלו, והמסלול ``md-heavy`` אינו מרים את MongoDB ואת Redis (``services`` של ``unit-tests``).
- **Unit Tests (3.11/3.12)** ו-**Unit Tests md-heavy (3.11/3.12)** – טסטי היחידה, בשני מסלולים לכל גרסת פייתון שרצים במקביל. ``Unit Tests`` מריץ את כל הטסטים חוץ מאלה שמסומנים ``md_heavy``, ו-``Unit Tests md-heavy`` מריץ רק אותם: טסטי פרסר ה-Markdown הכבדים (ראו :doc:`testing`). כל מסלול מדווח סטטוס חובה משלו, והמסלול ``md-heavy`` אינו מרים את MongoDB ואת Redis (``services`` של ``unit-tests``). coverage נמדד ונשלח ל-Codecov רק במסלולים שהמטריצה מסמנת ב-``coverage`` (רשומת ``include`` לפי גרסת פייתון), כדי לא לשלם פעמיים על ההאטה שהוא מוסיף לטסטים, וההעלאות של אותו קומיט מתמזגות ב-Codecov לדוח אחד.
- **JS Tests (node)** – טסטי הצד-לקוח שב-``tests/*.test.js``. כל קובץ הוא סקריפט עצמאי שמריץ את עצמו ויוצא עם קוד שגיאה בכשל, בלי רץ טסטים חיצוני. ``repo-history.test.js`` מדולג במפורש: הוא כתוב בסגנון ``describe``/``it`` ואין בפרויקט רץ שמספק אותם. **אינו סטטוס נדרש** – הכשל מופיע ב-PR אך אינו חוסם מיזוג.
- **Performance Tests** – טסטי ביצועים (ברירת מחדל: הכל; Draft + ``perf-light``: רק קלים). דוחות זמני ריצה נשמרים כארטיפקטים.

Expand Down
10 changes: 10 additions & 0 deletions docs/testing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,15 @@ Mocking HTTP ב‑github_menu_handler
monkeypatch.setattr(gh, "_http_sync_request", fake_req)
assert gh.http_request("POST", "https://example.com", data=b"x") is sentinel

השהיות של קוד הייצור בטסטים
---------------------------

**קוד הייצור ממתין בכוונה, והטסטים לא ממתינים איתו.** ה-rate limit בין קריאות ל-GitHub (``apply_rate_limit_delay`` ב-``github_menu_handler.py``), ה-retry עם backoff (``resilience.py``, ``http_sync.py``) ותקרת ההמתנה לתשובה ב-``http_async.py`` ממתינים באמת, וטסט שעובר בהם ממתין ועובר בלי שאיש יראה. לכן ``tests/conftest.py`` קובע להם ערכי בדיקה פעם אחת, בבלוק משתני הסביבה שלו. הוא קובע אותם בהשמה ולא ב-``setdefault``, ולכן הם חלים על כל הרצה — ``ci.yml``, ``deploy.yml`` והרצה מקומית — גם כשהמעטפת מייצאת ערך אחר, ומאותה סיבה הם לא נקבעים ב-env של workflow.

- **טסט שהנושא שלו הוא ההשהיה עצמה קובע אותה בעצמו** ב-``monkeypatch.setenv``, ולא נשען על ברירת המחדל של הייצור או של ``tests/conftest.py``. כך עושים ``tests/test_github_menu_backoff_delay.py`` ו-``tests/test_http_sync_adapter_retries.py``.
- **השהיה חדשה בקוד הייצור מקבלת ערך בדיקה באותו בלוק, באותו PR.** לא מנטרלים אותה בקובץ הטסטים שבמקרה שם לב אליה, כי הקובץ הבא לא יידע על כך.
- **המתנה אמיתית גם מסתירה באגים, ולא רק עולה זמן.** טסט שבודק התנהגות "באותה שנייה" לא מגיע אליה כשכל קריאה ממתינה. ``test_backup_id_is_unique_within_the_same_second`` עבר גם כשההגנה שהוא בודק הוסרה, כל עוד ההשהיה רצה.

רישום Blueprint בסביבת טסטים
------------------------------

Expand Down Expand Up @@ -212,6 +221,7 @@ Mocking HTTP ב‑github_menu_handler
--------------------------

- הפרויקט מגדיר ``pytest-cov`` ב-``pytest.ini``. אם חסר, התקינו: ``pip install pytest-cov``.
- ב-CI הוא נמדד רק בחלק מהמסלולים של ``unit-tests`` — ראו :doc:`ci-cd`.
- דוחות:

.. code-block:: bash
Expand Down
2 changes: 2 additions & 0 deletions docs/whats-new.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ What's New

2026-10-04
----------
- **ci: coverage נמדד רק במסלולי 3.11 של ``unit-tests``.** הוא הוסיף לטסטים של 3.12 כ-30% מהזמן שלהם (הערכה: מדידה מקומית עם coverage ובלעדיו, על זמני הטסטים מדוחות ``unit-durations-*`` של ריצת ``ci.yml`` מ-4.10), ובאותה ריצה התוצאות של 3.11 ושל 3.12 היו זהות, טסט מול טסט. עכשיו רשומת ``include`` במטריצה מוסיפה ``coverage: true`` לגרסה אחת, והדגלים ``--cov`` וההעלאה ל-Codecov תלויים בה. הגרסה היא 3.11, זו שהפרודקשן רץ עליה (``Dockerfile``). שמות הסטטוסים לא השתנו. ``tests/test_required_checks_are_listed.py`` פורש עכשיו את ``include`` לפי הכלל של GitHub, ובודק שלכל מסלול יש גרסה שמודדת coverage ושהדגלים וההעלאה תלויים במפתח הזה, כי Codecov אינו בדיקת חובה, וכיסוי שנעלם לא היה מפיל דבר. ראו :doc:`ci-cd`.
- **tests: השהיות של קוד הייצור מתאפסות פעם אחת לכל הטסטים, ב-``tests/conftest.py``.** ``apply_rate_limit_delay`` ממתינה בין קריאות ל-GitHub, וטסטי הורדת ה-ZIP עברו בה באמת: כ-98 שניות של המתנה בכל מסלול ``Unit Tests``, לפי דוחות ``unit-durations-*`` של ריצת ``ci.yml`` מ-4.10. שני קבצים אחרים כבר נטרלו אותה, כל אחד לעצמו, והקבצים האלה לא. עכשיו ``tests/conftest.py`` מאפס את ``GITHUB_API_BASE_DELAY`` ואת ``GITHUB_BACKOFF_DELAY``, והנטרולים הידניים נמחקו. אליו עברו גם ערכי ה-retry וה-backoff שהיו ב-env של ``ci.yml`` (``HTTP_RESILIENCE_*``, ``REQUESTS_RETRIES``, ``REQUESTS_RETRY_BACKOFF``, ``AIOHTTP_TIMEOUT_TOTAL``), כך שהם חלים עכשיו גם על ``deploy.yml`` ועל הרצה מקומית. **ההמתנה גם הסתירה שני טסטים, וזה נמדד במוטציות:** ``test_backup_id_is_unique_within_the_same_second`` לא הגיע אף פעם לשתי אריזות באותה שנייה, ועבר גם בלי הסיומת האקראית של המזהה. ו-``test_apply_rate_limit_delay_respects_backoff`` עבר גם כשה-backoff לא הופעל, כי ההשהיה הבסיסית הייתה ארוכה ממנו. עכשיו הראשון בודק את מה שהוא מבטיח, השני קובע את ההשהיה הבסיסית בעצמו, וטסט חדש עובר דרך ההמתנה הבסיסית האמיתית, בערך קטן שהוא קובע. **ומשטסט ה-backoff התחיל לבדוק באמת, הוא נכשל ב-CI לפי סדר הריצה:** ``test_telegram_retry_after_and_message_not_modified`` הוציא את ``github_menu_handler`` מ-``sys.modules`` וייבא אותו מחדש בלי להחזיר את המודול הקודם, וטסט ה-backoff, שרץ אחריו באותו תהליך, שינה את מצב ה-backoff במודול שהקוד שרץ לא קורא ממנו. עכשיו ``monkeypatch.delitem`` מחזיר את המודול בסוף הטסט, וטסט ה-backoff בונה את ``GitHubMenuHandler`` מאותו מודול שהוא משנה. ובאותו סבב, ``tests/conftest.py`` קובע את ערכי ההשהיה בהשמה ולא ב-``setdefault``, כדי שערך שהמעטפת מייצאת לא יחזיר את ההמתנה. **ובריצה אחרת של ה-CI נכשלו לפי סדר הריצה טסטים של** ``log_user_activity``\ **, מאותה משפחה:** ``tests/test_user_stats_unit.py`` עשה ``importlib.reload`` ל-``user_stats``, וה-reload בנה מופע חדש של ``user_stats.user_stats`` בזמן ש-``main.py`` ממשיך להחזיק את הקודם, ולכן טסט שהחליף את ``log_user`` במופע החדש לא הגיע לקוד של ``main``. ה-reload היה מיותר, כי ``user_stats`` מייבא את ה-facade בכל קריאה, והוא הוסר. ראו :doc:`testing`.
- **ci: טסטי ה-Markdown הכבדים רצים במסלול נפרד, במקביל לשאר טסטי היחידה.** בחודש שעבר זמן הטסטים ב-CI עלה מכשלוש דקות וחצי לכשבע וחצי (שלב ה-pytest בג'וב של 3.12, לפי דוחות ``unit-durations-*`` של ריצות ``ci.yml`` מ-9.9 ומ-4.10). הטסטים שהיו קיימים לא הואטו בסך הכול; את התוספת הביאו טסטים חדשים, ואת החלק הגדול שלה מעט טסטים כבדים. ביניהם טסטי פרסר ה-Markdown שעיקר העבודה שלהם הוא החישוב עצמו: השוואה לאורקל cmark-gfm, קלט ענק בתקרות של הפרסר, והטסטים של סקריפטי המדידה. הם מסומנים עכשיו ``md_heavy`` (הרישום ב-``pytest.ini``), והג'וב ``unit-tests`` ב-``.github/workflows/ci.yml`` רץ בשני מסלולים לכל גרסת פייתון: ``Unit Tests`` מריץ את כל השאר, ו-``Unit Tests md-heavy`` מריץ רק אותם, במקביל ובלי MongoDB ו-Redis שהוא אינו צריך. כל מסלול מדווח סטטוס חובה משלו. **את שני הסטטוסים החדשים צריך לסמן כבדיקות חובה בהגדרות של GitHub**, ועד שזה נעשה כשל בהם מופיע ב-PR אך אינו חוסם מיזוג. ושמות הסטטוסים, שכתובים ביד בכמה קבצים, נבדקים עכשיו מול המטריצה ב-``tests/test_required_checks_are_listed.py`` — והוא נכשל גם כשעותק של הרשימה מופיע בקובץ שהוא לא מכיר. :doc:`testing` ו-:doc:`branch-protection-and-pr-rules` מפנים עכשיו לרשימה שב-:doc:`ci-cd` במקום להחזיק עותק משלהם.
- **fix (שירות ה-MCP): ריפו שהוסר בממשק נמחק גם מהדיסק של שירות ה-MCP.** הסרת ריפו בוובאפ מחקה את המראה שבדיסק של הוובאפ ואת הרשומות ב-``repo_metadata``, אבל לדיסק של שירות ה-MCP אף אחד לא נגע: המראה שם נשארה, וכלי הקריאה המשיכו להגיש אותה כאילו כלום לא קרה (כך קרה ל-``codekeeper-plugin``). עכשיו בסוף כל מעבר של ה-autosync, כל מראה בדיסק של השירות שאין לה רשומה ב-``repo_metadata`` נמחקת, דרך ``delete_mirror``. שום דבר לא נמחק כשהשאילתה נכשלה, או כשאף שם ב-``repo_metadata`` אינו שם של מראה שבדיסק (כולל קריאה ריקה). כשיש בדיסק מראות שנחסכו כך, נרשמת אזהרה. וגם לא נמחקים תיקייה שאינה מראה שהשירות יוצר (שם לא תקין, קישור סמלי) או ריפו שנמצא באמצע clone או fetch. מחיקה שנכשלה, או תיקיית מראות שאי אפשר לקרוא, נספרות ב-``errors``, והמראה נשארת עד המעבר הבא. הקריאה של ``repo_metadata`` נעשית מה-primary במפורש. בשורת הלוג ``repo autosync pass:`` נוסף המונה ``pruned``. ובאותה מנייה של המראות, גם ניקוי ה-credentials בעלייה מדווח עכשיו על תיקיית מראות שאי אפשר לקרוא ככשל, ולא כ-``checked=0 failed=0``. ראו :ref:`mcp-repo-autosync`.
- **fix (מראות): טוקן ה-GitHub לא נשמר יותר בקובץ ה-config של המראות, ולא עובר בשורת הפקודה (#3480); וזיהוי הענף הראשי בייבוא ריפו (#3479).** עד עכשיו ``init_mirror`` שכפל מ-URL שהטוקן בתוכו, ו-git שמר אותו בטקסט גלוי ב-``remote.origin.url`` — על הדיסקים של הוובאפ ושל שירות ה-MCP, ובצילומים היומיים של Render; הוא גם הופיע בשורת הפקודה של ``git-remote-http`` בכל clone ו-fetch. עכשיו ה-URL השמור נקי, והטוקן עובר ככותרת דרך משתני סביבה של git, ורק לריפו שדורש הזדהות — ריפו ציבורי לא מקבל טוקן. מראות קיימות מנוקות לפני כל fetch ובעליית כל שירות, עם שורת לוג אחת שסופרת מה נמצא ומה נוקה, ואומרת לכל מראה מאיפה הטוקן שלה (``map``/``global``). fetch על מראה שלא אומת שהיא נקייה לא רץ (``mirror_url_not_clean``). ובנפרד: ``initial_import`` ניסה לזהות את הענף הראשי בפקודות ש-``_run_git_command`` דוחה, ולכן תמיד נפל ל-``main`` — וריפו שהענף הראשי שלו ``master`` לא יובא. עכשיו הזיהוי הוא מ-HEAD של המראה, וכשאין ענף מאומת מוחזר ``default_branch_undetected`` במקום ניחוש. ראו :ref:`mcp-mirror-credentials`.
Expand Down
25 changes: 25 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,31 @@
os.environ.setdefault('REDIS_CONNECT_TIMEOUT', '0.25')
os.environ.setdefault('REDIS_SOCKET_TIMEOUT', '0.25')

# אותו עיקרון להמתנות שקוד הייצור עושה בכוונה: rate limit בין קריאות ל-GitHub
# (``apply_rate_limit_delay`` ב-``github_menu_handler.py``), retry עם backoff
# (``resilience.py``, ``http_sync.py``), ותקרת ההמתנה לתשובה ב-``http_async.py``. טסט שעובר
# בהן ממתין באמת ועובר, ואיש לא רואה — עד שמישהו קורא דוח זמנים. הערכים נקבעים כאן, ולא ב-env של
# workflow, כדי שיחולו על כל הרצה: ``ci.yml``, ``deploy.yml`` והרצה מקומית. הם לפני ייבוא
# ה-stubs, כי ``resilience.py`` קורא את שלו כבר בזמן הייבוא (``DEFAULT_RETRY_POLICY``).
#
# המתנה אמיתית גם מסתירה באגים, ולא רק עולה זמן: טסט שבודק התנהגות "באותה שנייה" לא מגיע
# אליה כשכל קריאה ממתינה. טסט שהנושא שלו הוא ההשהיה עצמה קובע אותה בעצמו
# (``monkeypatch.setenv``), כמו ב-``tests/test_github_menu_backoff_delay.py``. והשהיה חדשה
# בקוד הייצור מקבלת כאן ערך בדיקה באותו PR, ולא נטרול בקובץ הטסטים שבמקרה שם לב אליה.
#
# השמה ולא ``setdefault`` כמו בשאר הבלוק: כאן אין ערך מבחוץ שהטסטים צריכים לכבד, ו-
# ``setdefault`` היה משאיר ערך שהמעטפת מייצאת — למשל כשמריצים באותה מעטפת גם את הבוט —
# ומחזיר בשקט את ההמתנה האמיתית.
os.environ['GITHUB_API_BASE_DELAY'] = '0'
os.environ['GITHUB_BACKOFF_DELAY'] = '0'
os.environ['HTTP_RESILIENCE_MAX_ATTEMPTS'] = '1'
os.environ['HTTP_RESILIENCE_BACKOFF_BASE'] = '0'
os.environ['HTTP_RESILIENCE_BACKOFF_MAX'] = '0'
os.environ['HTTP_RESILIENCE_JITTER'] = '0'
os.environ['REQUESTS_RETRIES'] = '0'
os.environ['REQUESTS_RETRY_BACKOFF'] = '0'
os.environ['AIOHTTP_TIMEOUT_TOTAL'] = '6'

# Import stubs so any import of `telegram` succeeds in tests
try:
import tests._telegram_stubs # noqa: F401
Expand Down
Loading
Loading