Suite time for the unit tests dropped from ~14s to 0.035s once the live network calls were stubbed.
Bugs found — pinned, not fixed
Each is covered by a test with a KNOWN BUG docstring so a fix will flip the test:
qbit-maid.py:92 — self.ignored_tags = self.config["ignored_domains"]; the [ignored_tags] section is never read, so tag-based ignoring silently does nothing.
qlist.is_ignored_tag — substring match, not tag match: 'ipt' matches 'iptorrents', 'save' matches 'unsaved'.
qbit-maid.py:120 — self.po.send_message(...) but the vendored pushover module has no module-level send_message; a qBittorrent auth failure raises AttributeError instead of notifying.
qlogging.list_first_tor — indexes torrent_list[0] unconditionally; an instance with zero torrents raises IndexError and aborts the run before any summary is sent.
qlist.build_tor_list — the blank-tag block and the tracker branch both call torrents_add_tags, so non-premature new torrents get tagged twice.
AppriseClient.__init__ — bare except: KeyError swallows the DOCKER KeyError and skips the rest of the try block, so toml_path is only read when DOCKERand all five env vars are set.
qlogging.setup_logging — removes old handlers without closing them, leaking a file descriptor per call.
Also noted, not changed: config.toml.example is missing the [telemetry] section that qbit-maid.py:82 requires, so a config copied from the example crashes on startup; and requirements.txt pins qbittorrent_api==2022.5.32, which cannot import on Python 3.13 (the Dockerfile ignores it and installs unpinned).
Happy to fix any of these — say which.
## Why
The suite looked green but proved little:
- **8 empty tests** (`pass` bodies) covering `is_preme`, `is_cat_ignored`, `is_tracker_blank`, `is_protected_tracker`, `is_not_protected_tracker`, `is_protected_under_ratio`, `is_protected_over_ratio`, `is_non_protected_tor`.
- **2 tests made live network calls** to `https://thunder.jonb.io/ping/<uuid>` — a real healthcheck endpoint with production side effects.
- `test_write_csv` only asserted `os.path.exists`.
- **CI never ran the main suite** — `.gitea/workflows/test.yml` only ran `test_komodo_deploy`.
## What changed
| File | Change |
|---|---|
| `qbt_fakes.py` | new — shared fakes: `FakeQbt`, `FakeQbtClient`, `FakeAPIError`, `StubPushover`, `FailingPushover`, `torrent()` factory |
| `test_qlist.py` | new — tag/tracker/ratio predicates, boundary cases |
| `test_qprocess.py` | new — file deletion, dry-run, error paths |
| `test_qlogging_extra.py` | new — summary formatting, pushover/apprise dispatch, logging setup |
| `test_apprise.py` | new — env/TOML config resolution, send paths |
| `test_qbt.py` | new — loads hyphenated `qbit-maid.py` via `importlib`, covers the `__main__` entry point |
| `test_qbitmaid.py` | rewritten — pipeline tests against `FakeQbt` |
| `test_write_csv.py` | rewritten — header-once, append, round-trip content in temp dirs |
| `.coveragerc` | new — source scope + report config |
| `.gitea/workflows/test.yml` | new `unit-tests` job: installs `requests qbittorrent-api coverage` (mirroring the Dockerfile) and runs the full suite with `--fail-under=100` |
## Verification
```
Ran 142 tests in 14.035s
OK
Name Stmts Miss Cover
AppriseClient.py 35 0 100%
qbit-maid.py 95 0 100%
qlist.py 63 0 100%
qlogging.py 188 0 100%
qprocess.py 59 0 100%
TOTAL 440 0 100%
```
Mutation check: **15/15 caught** (boundary flips, guard removal, separator/emoji/html/priority changes, csv header, delete-files flag).
Suite time for the unit tests dropped from ~14s to **0.035s** once the live network calls were stubbed.
## Bugs found — pinned, not fixed
Each is covered by a test with a `KNOWN BUG` docstring so a fix will flip the test:
1. `qbit-maid.py:92` — `self.ignored_tags = self.config["ignored_domains"]`; the `[ignored_tags]` section is never read, so tag-based ignoring silently does nothing.
2. `qlist.is_ignored_tag` — substring match, not tag match: `'ipt'` matches `'iptorrents'`, `'save'` matches `'unsaved'`.
3. `qbit-maid.py:120` — `self.po.send_message(...)` but the vendored `pushover` module has no module-level `send_message`; a qBittorrent auth failure raises `AttributeError` instead of notifying.
4. `qlogging.list_first_tor` — indexes `torrent_list[0]` unconditionally; an instance with zero torrents raises `IndexError` and aborts the run before any summary is sent.
5. `qlist.build_tor_list` — the blank-tag block and the tracker branch both call `torrents_add_tags`, so non-premature new torrents get tagged twice.
6. `AppriseClient.__init__` — bare `except: KeyError` swallows the `DOCKER` KeyError and skips the rest of the try block, so `toml_path` is only read when `DOCKER` **and** all five env vars are set.
7. `qlogging.setup_logging` — removes old handlers without closing them, leaking a file descriptor per call.
Also noted, not changed: `config.toml.example` is missing the `[telemetry]` section that `qbit-maid.py:82` requires, so a config copied from the example crashes on startup; and `requirements.txt` pins `qbittorrent_api==2022.5.32`, which cannot import on Python 3.13 (the Dockerfile ignores it and installs unpinned).
Happy to fix any of these — say which.
The suite had 8 empty `pass` tests, two tests that made live network
calls to a production healthcheck endpoint, and a write_csv test that
only asserted the file existed. CI ran only test_komodo_deploy, so the
main suite never executed at all.
- Split tests by module: test_qlist, test_qprocess, test_qlogging_extra,
test_apprise, test_qbt; rewrote test_qbitmaid and test_write_csv.
- qbt_fakes.py: shared fakes (FakeQbt, FakeQbtClient, FakeAPIError,
StubPushover, FailingPushover, torrent factory).
- test_qbt.py loads the hyphenated qbit-maid.py via importlib and covers
the __main__ entry point.
- Replaced the live-network tests with a RecordingRequests stub
(suite time 14s -> 0.035s).
- .coveragerc + CI unit-tests job running the full suite with
--fail-under=100.
127 unit tests, 100% statement coverage on all 5 first-party modules
(440 stmts), 15/15 mutations caught.
Seven pre-existing bugs are pinned by tests with KNOWN BUG docstrings
and left unfixed pending a decision.
The runner image ships pip 24.0 from the Debian package, so
`pip install --upgrade pip` aborts with "Cannot uninstall pip 24.0,
RECORD file not found" and the job failed after 4s. Drop the upgrade
and install with --break-system-packages.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Why
The suite looked green but proved little:
passbodies) coveringis_preme,is_cat_ignored,is_tracker_blank,is_protected_tracker,is_not_protected_tracker,is_protected_under_ratio,is_protected_over_ratio,is_non_protected_tor.https://thunder.jonb.io/ping/<uuid>— a real healthcheck endpoint with production side effects.test_write_csvonly assertedos.path.exists..gitea/workflows/test.ymlonly rantest_komodo_deploy.What changed
qbt_fakes.pyFakeQbt,FakeQbtClient,FakeAPIError,StubPushover,FailingPushover,torrent()factorytest_qlist.pytest_qprocess.pytest_qlogging_extra.pytest_apprise.pytest_qbt.pyqbit-maid.pyviaimportlib, covers the__main__entry pointtest_qbitmaid.pyFakeQbttest_write_csv.py.coveragerc.gitea/workflows/test.ymlunit-testsjob: installsrequests qbittorrent-api coverage(mirroring the Dockerfile) and runs the full suite with--fail-under=100Verification
Mutation check: 15/15 caught (boundary flips, guard removal, separator/emoji/html/priority changes, csv header, delete-files flag).
Suite time for the unit tests dropped from ~14s to 0.035s once the live network calls were stubbed.
Bugs found — pinned, not fixed
Each is covered by a test with a
KNOWN BUGdocstring so a fix will flip the test:qbit-maid.py:92—self.ignored_tags = self.config["ignored_domains"]; the[ignored_tags]section is never read, so tag-based ignoring silently does nothing.qlist.is_ignored_tag— substring match, not tag match:'ipt'matches'iptorrents','save'matches'unsaved'.qbit-maid.py:120—self.po.send_message(...)but the vendoredpushovermodule has no module-levelsend_message; a qBittorrent auth failure raisesAttributeErrorinstead of notifying.qlogging.list_first_tor— indexestorrent_list[0]unconditionally; an instance with zero torrents raisesIndexErrorand aborts the run before any summary is sent.qlist.build_tor_list— the blank-tag block and the tracker branch both calltorrents_add_tags, so non-premature new torrents get tagged twice.AppriseClient.__init__— bareexcept: KeyErrorswallows theDOCKERKeyError and skips the rest of the try block, sotoml_pathis only read whenDOCKERand all five env vars are set.qlogging.setup_logging— removes old handlers without closing them, leaking a file descriptor per call.Also noted, not changed:
config.toml.exampleis missing the[telemetry]section thatqbit-maid.py:82requires, so a config copied from the example crashes on startup; andrequirements.txtpinsqbittorrent_api==2022.5.32, which cannot import on Python 3.13 (the Dockerfile ignores it and installs unpinned).Happy to fix any of these — say which.