snapshot of https://github.com/zulip/zulip.git @ 48c69911c7dd4e03b20552eb3bd77ad8151c1357 (2026-09-15)
Some checks failed
Code scanning / CodeQL (push) Failing after 15s
Zulip production suite / Ubuntu 22.04 production build (push) Failing after 26s
Zulip production suite / Debian 12 production install with custom db name and user (push) Has been skipped
Zulip production suite / Ubuntu 22.04 production install and PostgreSQL upgrade with pgroonga (push) Has been skipped
Zulip production suite / Ubuntu 24.04 production install (push) Has been skipped
Zulip production suite / Ubuntu 26.04 production install (push) Has been skipped
Zulip production suite / Debian 13 production install (push) Has been skipped
Zulip production suite / 7.0 Version Upgrade (push) Has been skipped
Zulip production suite / 8.0 Version Upgrade (push) Has been skipped
Zulip production suite / 6.0 Version Upgrade (push) Has been skipped
Zulip production suite / 10.0 Version Upgrade (push) Has been skipped
Zulip production suite / 9.0 Version Upgrade (push) Has been skipped
Zulip production suite / 12.0 Version Upgrade (push) Has been skipped
Zulip production suite / 11.0 Version Upgrade (push) Has been skipped
GitHub Actions Security Analysis with zizmor / zizmor (push) Failing after 3s
Zulip CI / Debian 12 (Python 3.11, backend + documentation) (push) Failing after 26s
Zulip CI / Ubuntu 22.04 (Python 3.10, backend + frontend) (push) Failing after 3s
Zulip CI / Ubuntu 24.04 (Python 3.12, backend) (push) Failing after 25s
Zulip CI / Ubuntu 26.04 (Python 3.14, backend) (push) Failing after 25s
Zulip CI / Debian 13 (Python 3.13, backend) (push) Failing after 28s
Zulip production suite / Required jobs (push) Has been cancelled
Zulip CI / Required jobs (push) Has been cancelled
API Documentation Update Check / check-feature-level-updated (push) Has been cancelled
API Documentation Update Check / notify-if-api-docs-changed (push) Has been cancelled
Some checks failed
Code scanning / CodeQL (push) Failing after 15s
Zulip production suite / Ubuntu 22.04 production build (push) Failing after 26s
Zulip production suite / Debian 12 production install with custom db name and user (push) Has been skipped
Zulip production suite / Ubuntu 22.04 production install and PostgreSQL upgrade with pgroonga (push) Has been skipped
Zulip production suite / Ubuntu 24.04 production install (push) Has been skipped
Zulip production suite / Ubuntu 26.04 production install (push) Has been skipped
Zulip production suite / Debian 13 production install (push) Has been skipped
Zulip production suite / 7.0 Version Upgrade (push) Has been skipped
Zulip production suite / 8.0 Version Upgrade (push) Has been skipped
Zulip production suite / 6.0 Version Upgrade (push) Has been skipped
Zulip production suite / 10.0 Version Upgrade (push) Has been skipped
Zulip production suite / 9.0 Version Upgrade (push) Has been skipped
Zulip production suite / 12.0 Version Upgrade (push) Has been skipped
Zulip production suite / 11.0 Version Upgrade (push) Has been skipped
GitHub Actions Security Analysis with zizmor / zizmor (push) Failing after 3s
Zulip CI / Debian 12 (Python 3.11, backend + documentation) (push) Failing after 26s
Zulip CI / Ubuntu 22.04 (Python 3.10, backend + frontend) (push) Failing after 3s
Zulip CI / Ubuntu 24.04 (Python 3.12, backend) (push) Failing after 25s
Zulip CI / Ubuntu 26.04 (Python 3.14, backend) (push) Failing after 25s
Zulip CI / Debian 13 (Python 3.13, backend) (push) Failing after 28s
Zulip production suite / Required jobs (push) Has been cancelled
Zulip CI / Required jobs (push) Has been cancelled
API Documentation Update Check / check-feature-level-updated (push) Has been cancelled
API Documentation Update Check / notify-if-api-docs-changed (push) Has been cancelled
This commit is contained in:
101
.claude/skills/fix-backend-coverage/SKILL.md
Normal file
101
.claude/skills/fix-backend-coverage/SKILL.md
Normal file
@@ -0,0 +1,101 @@
|
||||
---
|
||||
name: fix-backend-coverage
|
||||
description: Fix backend test coverage gaps. Use when CI output or test-backend --coverage reports missing lines, like "ERROR: path/to/file.py no longer has complete backend test coverage".
|
||||
argument-hint: "[test_module_or_file]"
|
||||
---
|
||||
|
||||
# Fix Backend Coverage
|
||||
|
||||
Fix backend test coverage gaps for Zulip's enforced 100% coverage files.
|
||||
|
||||
Use this skill when:
|
||||
|
||||
- CI output contains "no longer has complete backend test coverage"
|
||||
- You need to verify coverage after modifying code in an enforced file
|
||||
- A file in `not_yet_fully_covered` has reached 100% and should be
|
||||
promoted to enforced, or coverage should be added to reach 100%
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Run coverage on the specific test module
|
||||
|
||||
IMPORTANT: Never run the full test suite with `--coverage` — it takes
|
||||
a very long time. Always target the specific test module that covers
|
||||
the file with missing coverage.
|
||||
|
||||
```bash
|
||||
./tools/test-backend --skip-provision-check --coverage --no-cov-cleanup \
|
||||
--no-html-report zerver.tests.test_specific_module
|
||||
```
|
||||
|
||||
If invoked with an argument, use it: `$ARGUMENTS`
|
||||
|
||||
If the CI output names the source file but not the test module, use
|
||||
`git grep` to find which test file imports or tests the relevant code.
|
||||
|
||||
### 2. Analyze missing lines
|
||||
|
||||
```bash
|
||||
./.claude/skills/fix-backend-coverage/analyze-coverage <source_file_with_missing_coverage>
|
||||
```
|
||||
|
||||
With no arguments, checks all enforced files for missing coverage
|
||||
(useful after a targeted test run to see what's still missing).
|
||||
|
||||
The script loads `var/.coverage` and reports:
|
||||
|
||||
- Classification: ENFORCED (must be 100%) vs EXEMPT
|
||||
- Statement/excluded/missing counts and coverage percentage
|
||||
- Each missing line with 1 line of source context above and below
|
||||
|
||||
### 3. Fix each uncovered line using the right technique
|
||||
|
||||
Read the source at each missing line and classify it:
|
||||
|
||||
| Line type | Fix |
|
||||
| ----------------------------------- | ----------------------------------------------------------- |
|
||||
| Dead code (unreachable branch) | Simplify/remove the dead branch |
|
||||
| Error-only test path (assert, fail) | Add `# nocoverage` comment |
|
||||
| Missing test coverage | Write a test that exercises the line |
|
||||
| Newly 100% covered file | Remove from `not_yet_fully_covered` in `tools/test-backend` |
|
||||
|
||||
`# nocoverage` should only be used for lines that execute only when a
|
||||
test fails or for truly unreachable defensive code. Never use it to
|
||||
skip writing tests for reachable production code.
|
||||
|
||||
### 4. Verify
|
||||
|
||||
Re-run coverage on the same targeted test module and re-analyze:
|
||||
|
||||
```bash
|
||||
./tools/test-backend --skip-provision-check --coverage --no-cov-cleanup \
|
||||
--no-html-report zerver.tests.test_specific_module
|
||||
./.claude/skills/fix-backend-coverage/analyze-coverage <source_file>
|
||||
```
|
||||
|
||||
Confirm 0 missing lines, then lint:
|
||||
|
||||
```bash
|
||||
./tools/lint --skip-provision-check --fix --only=ruff,ruff-format <changed_files>
|
||||
```
|
||||
|
||||
## Coverage system reference
|
||||
|
||||
### Config
|
||||
|
||||
- Coverage config: `tools/coveragerc`
|
||||
- Coverage data: `var/.coverage`
|
||||
- Exclusion patterns in `coveragerc`: `# nocoverage`, `if False:`,
|
||||
`raise NotImplementedError`, `raise AssertionError`, `if TYPE_CHECKING:`,
|
||||
`@abstractmethod`, `@skip`, and `...` (ellipsis)
|
||||
|
||||
### Enforcement model
|
||||
|
||||
- `tools/test-backend` defines two lists:
|
||||
- `enforce_fully_covered`: all `.py` files matching `source_files` globs
|
||||
minus those in `not_yet_fully_covered`
|
||||
- `not_yet_fully_covered`: files exempt from the 100% requirement
|
||||
- When a file in `not_yet_fully_covered` reaches 100%, it should be
|
||||
removed from the list to promote it to enforced status
|
||||
- Enforcement only runs in CI with full suite + `--coverage`; locally
|
||||
you check with targeted runs + `analyze-coverage`
|
||||
223
.claude/skills/fix-backend-coverage/analyze-coverage
Executable file
223
.claude/skills/fix-backend-coverage/analyze-coverage
Executable file
@@ -0,0 +1,223 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Analyze backend test coverage and report missing lines with source context.
|
||||
|
||||
Usage:
|
||||
# First, generate coverage data:
|
||||
./tools/test-backend --coverage --no-cov-cleanup --no-html-report <module>
|
||||
|
||||
# Then analyze specific files:
|
||||
./.claude/fix-backend-coverage/analyze-coverage zerver/tests/test_import_export.py
|
||||
|
||||
# Or analyze all enforced files with missing coverage:
|
||||
./.claude/fix-backend-coverage/analyze-coverage
|
||||
"""
|
||||
|
||||
import glob
|
||||
import os
|
||||
import sys
|
||||
|
||||
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT_DIR = os.path.dirname(os.path.dirname(SCRIPT_DIR))
|
||||
os.chdir(ROOT_DIR)
|
||||
sys.path.insert(0, ROOT_DIR)
|
||||
|
||||
from tools.lib import sanity_check
|
||||
|
||||
sanity_check.check_venv(__file__)
|
||||
|
||||
import coverage
|
||||
|
||||
# These lists are duplicated from tools/test-backend so that this script
|
||||
# can run independently. Keep them in sync.
|
||||
source_files = [
|
||||
"analytics/**/*.py",
|
||||
"confirmation/**/*.py",
|
||||
"corporate/**/*.py",
|
||||
"pgroonga/**/*.py",
|
||||
"zerver/**/*.py",
|
||||
"zilencer/**/*.py",
|
||||
"zproject/**/*.py",
|
||||
]
|
||||
|
||||
not_yet_fully_covered = [
|
||||
"*/migrations/*.py",
|
||||
"*/management/commands/*.py",
|
||||
"analytics/lib/fixtures.py",
|
||||
"analytics/views/stats.py",
|
||||
"corporate/views/installation_activity.py",
|
||||
"corporate/views/plan_activity.py",
|
||||
"corporate/views/realm_activity.py",
|
||||
"corporate/views/remote_billing_page.py",
|
||||
"corporate/views/audit_logs.py",
|
||||
"corporate/views/support.py",
|
||||
"corporate/lib/activity.py",
|
||||
"corporate/lib/remote_billing_util.py",
|
||||
"zerver/lib/addressee.py",
|
||||
"zerver/lib/markdown/__init__.py",
|
||||
"zerver/lib/cache.py",
|
||||
"zerver/lib/cache_helpers.py",
|
||||
"zerver/lib/i18n.py",
|
||||
"zerver/lib/send_email.py",
|
||||
"zerver/lib/url_preview/preview.py",
|
||||
"zerver/lib/markdown/api_arguments_table_generator.py",
|
||||
"zerver/lib/markdown/fenced_code.py",
|
||||
"zerver/lib/markdown/nested_code_blocks.py",
|
||||
"zerver/worker/deferred_work.py",
|
||||
"zerver/worker/missedmessage_emails.py",
|
||||
"zerver/worker/base.py",
|
||||
"zerver/worker/queue_processors.py",
|
||||
"zerver/worker/test.py",
|
||||
"zerver/middleware.py",
|
||||
"zerver/lib/bot_lib.py",
|
||||
"zerver/lib/camo.py",
|
||||
"zerver/lib/debug.py",
|
||||
"zerver/lib/export.py",
|
||||
"zerver/lib/fix_unreads.py",
|
||||
"zerver/lib/import_realm.py",
|
||||
"zerver/lib/logging_util.py",
|
||||
"zerver/lib/profile.py",
|
||||
"zerver/lib/queue.py",
|
||||
"zerver/lib/sqlalchemy_utils.py",
|
||||
"zerver/lib/storage.py",
|
||||
"zerver/lib/templates.py",
|
||||
"zerver/lib/generate_test_data.py",
|
||||
"zerver/lib/server_initialization.py",
|
||||
"zerver/lib/test_fixtures.py",
|
||||
"zerver/lib/test_runner.py",
|
||||
"zerver/lib/test_console_output.py",
|
||||
"zerver/lib/zstd_level9.py",
|
||||
"zerver/openapi/curl_param_value_generators.py",
|
||||
"zerver/openapi/javascript_examples.py",
|
||||
"zerver/openapi/python_examples.py",
|
||||
"zerver/openapi/test_curl_examples.py",
|
||||
"zerver/openapi/merge_api_changelogs.py",
|
||||
"zerver/tornado/descriptors.py",
|
||||
"zerver/tornado/django_api.py",
|
||||
"zerver/tornado/event_queue.py",
|
||||
"zerver/tornado/exceptions.py",
|
||||
"zerver/tornado/handlers.py",
|
||||
"zerver/tornado/ioloop_logging.py",
|
||||
"zerver/tornado/sharding.py",
|
||||
"zerver/tornado/views.py",
|
||||
"zerver/data_import/slack.py",
|
||||
"zerver/data_import/import_util.py",
|
||||
"zerver/webhooks/greenhouse/view.py",
|
||||
"zerver/webhooks/jira/view.py",
|
||||
"zerver/webhooks/teamcity/view.py",
|
||||
"zerver/webhooks/travis/view.py",
|
||||
"zerver/webhooks/zapier/view.py",
|
||||
"zerver/views/sentry.py",
|
||||
"zerver/lib/safe_session_cached_db.py",
|
||||
"zerver/lib/singleton_bmemcached.py",
|
||||
"zerver/lib/migrate.py",
|
||||
"zproject/computed_settings.py",
|
||||
"zproject/custom_dev_settings.py",
|
||||
"zproject/dev_settings.py",
|
||||
"zproject/test_extra_settings.py",
|
||||
"zproject/sentry.py",
|
||||
"zproject/wsgi.py",
|
||||
]
|
||||
|
||||
enforce_fully_covered = sorted(
|
||||
{path for target in source_files for path in glob.glob(target, recursive=True)}
|
||||
- {path for target in not_yet_fully_covered for path in glob.glob(target, recursive=True)}
|
||||
)
|
||||
|
||||
not_yet_set = {
|
||||
path for target in not_yet_fully_covered for path in glob.glob(target, recursive=True)
|
||||
}
|
||||
|
||||
|
||||
def classify_path(path: str) -> str:
|
||||
if path in enforce_fully_covered:
|
||||
return "ENFORCED - must be 100%"
|
||||
if path in not_yet_set:
|
||||
return "EXEMPT - in not_yet_fully_covered"
|
||||
return "UNKNOWN - not in source_files"
|
||||
|
||||
|
||||
def print_source_context(path: str, line_no: int) -> None:
|
||||
"""Print a missing line with 1 line of context above and below."""
|
||||
try:
|
||||
with open(path) as f:
|
||||
lines = f.readlines()
|
||||
except OSError:
|
||||
print(f" (could not read {path})")
|
||||
return
|
||||
|
||||
start = max(0, line_no - 2)
|
||||
end = min(len(lines), line_no + 1)
|
||||
for i in range(start, end):
|
||||
prefix = " >> " if i == line_no - 1 else " "
|
||||
print(f"{prefix}{i + 1:>5}| {lines[i].rstrip()}")
|
||||
|
||||
|
||||
def analyze_file(cov: coverage.Coverage, path: str) -> bool:
|
||||
"""Analyze coverage for a single file. Returns True if fully covered."""
|
||||
classification = classify_path(path)
|
||||
try:
|
||||
_filename, statements, excluded, missing, _formatted = cov.analysis2(path)
|
||||
except coverage.misc.NoSource:
|
||||
print(f"=== {path} [{classification}] ===")
|
||||
print(" No source found in coverage data.\n")
|
||||
return True
|
||||
|
||||
total = len(statements)
|
||||
excluded_count = len(excluded)
|
||||
missing_count = len(missing)
|
||||
covered = total - missing_count
|
||||
pct = (covered / total * 100) if total > 0 else 100.0
|
||||
|
||||
print(f"=== {path} [{classification}] ===")
|
||||
print(
|
||||
f"Statements: {total} | Excluded: {excluded_count} | Missing: {missing_count} | Coverage: {pct:.1f}%"
|
||||
)
|
||||
|
||||
if missing:
|
||||
print("\nMissing lines:")
|
||||
for line_no in missing:
|
||||
print()
|
||||
print_source_context(path, line_no)
|
||||
print()
|
||||
else:
|
||||
print(" All lines covered!\n")
|
||||
|
||||
if excluded:
|
||||
excluded_str = ", ".join(str(line) for line in excluded)
|
||||
print(f"Excluded lines: {excluded_str}\n")
|
||||
|
||||
return missing_count == 0
|
||||
|
||||
|
||||
def main() -> None:
|
||||
cov = coverage.Coverage(config_file="tools/coveragerc")
|
||||
try:
|
||||
cov.load()
|
||||
except coverage.misc.NoDataError:
|
||||
print("ERROR: No coverage data found at var/.coverage")
|
||||
print("Run tests with --coverage first:")
|
||||
print(" ./tools/test-backend --coverage --no-cov-cleanup --no-html-report <module>")
|
||||
sys.exit(1)
|
||||
|
||||
files = sys.argv[1:]
|
||||
if not files:
|
||||
# Analyze all enforced files that have missing coverage.
|
||||
print("No files specified; checking all enforced files for missing coverage...\n")
|
||||
any_missing = False
|
||||
for path in enforce_fully_covered:
|
||||
try:
|
||||
missing_lines = cov.analysis2(path)[3]
|
||||
except coverage.misc.NoSource:
|
||||
continue
|
||||
if missing_lines:
|
||||
analyze_file(cov, path)
|
||||
any_missing = True
|
||||
if not any_missing:
|
||||
print("All enforced files have 100% coverage!")
|
||||
else:
|
||||
for path in files:
|
||||
analyze_file(cov, path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user