test: audit suite for meaningfulness, reach 100% coverage
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.
This commit is contained in:
1 parent
5b54d9e558
commit
925f9619be
10 files changed
+1645
-461
No files matched your search
+158
@@ -0,0 +1,158 @@
|
||||
"""Shared fakes for the qbit-maid test suite.
|
||||
|
||||
This is test scaffolding, not production code, so it is excluded from the
|
||||
coverage report (see ``.coveragerc``).
|
||||
"""
|
||||
import datetime
|
||||
import logging
|
||||
from collections import Counter
|
||||
|
||||
# Keep test output clean: the pipeline logs warnings for ignored torrents.
|
||||
_test_logger = logging.getLogger("qbit-maid-test")
|
||||
_test_logger.addHandler(logging.NullHandler())
|
||||
_test_logger.propagate = False
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# qbittorrentapi stand-in
|
||||
# ---------------------------------------------------------------------------
|
||||
class FakeApp:
|
||||
version = "v4.6.2"
|
||||
web_api_version = "2.9.3"
|
||||
|
||||
|
||||
class FakeAPIError(Exception):
|
||||
"""Stands in for qbittorrentapi.APIError."""
|
||||
|
||||
|
||||
class FakeQbtClient:
|
||||
"""Records calls instead of talking to a real qBittorrent instance."""
|
||||
|
||||
def __init__(self, torrents=None, login_error=None, **kwargs):
|
||||
self.app = FakeApp()
|
||||
self._torrents = list(torrents or [])
|
||||
self._login_error = login_error
|
||||
self.init_kwargs = kwargs
|
||||
self.added_tags = []
|
||||
self.deleted_tags = []
|
||||
self.deleted = []
|
||||
self.logged_in = False
|
||||
|
||||
def auth_log_in(self):
|
||||
if self._login_error is not None:
|
||||
raise self._login_error
|
||||
self.logged_in = True
|
||||
|
||||
def torrents_info(self):
|
||||
return list(self._torrents)
|
||||
|
||||
def torrents_add_tags(self, tags, torrent_hash):
|
||||
self.added_tags.append((tags, torrent_hash))
|
||||
|
||||
def torrents_delete_tags(self, tags):
|
||||
self.deleted_tags.append(list(tags))
|
||||
|
||||
def torrents_delete(self, delete_files, hashes):
|
||||
self.deleted.append((delete_files, list(hashes)))
|
||||
|
||||
|
||||
def torrent(name="Example.Torrent.Release", tracker="https://tracker.example/announce",
|
||||
category="general", tags="", seeding_time=999999, state="uploading",
|
||||
ratio=2.0, time_active=999999, infohash="abc123", added_on=1):
|
||||
"""Build a torrent dict shaped like a qbittorrentapi response."""
|
||||
return {
|
||||
"name": name,
|
||||
"tracker": tracker,
|
||||
"category": category,
|
||||
"tags": tags,
|
||||
"seeding_time": seeding_time,
|
||||
"state": state,
|
||||
"ratio": ratio,
|
||||
"time_active": time_active,
|
||||
"infohash_v1": infohash,
|
||||
"hash": infohash,
|
||||
"added_on": added_on,
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Pushover stand-ins
|
||||
# ---------------------------------------------------------------------------
|
||||
class StubPushover:
|
||||
"""Records message() calls instead of hitting the Pushover API."""
|
||||
|
||||
def __init__(self):
|
||||
self.calls = []
|
||||
|
||||
def message(self, user, body, **kwargs):
|
||||
self.calls.append({"user": user, "body": body, "kwargs": kwargs})
|
||||
|
||||
|
||||
class FailingPushover:
|
||||
def message(self, *args, **kwargs):
|
||||
raise RuntimeError("pushover is down")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# The Qbt object
|
||||
# ---------------------------------------------------------------------------
|
||||
class FakeQbt:
|
||||
"""Stand-in for the Qbt object.
|
||||
|
||||
Defaults match the notifier tests (42 scanned, 7 removed, 4 orphaned).
|
||||
Pipeline tests override ``torrent_list`` and ``qbt_client``.
|
||||
"""
|
||||
|
||||
def __init__(self, **overrides):
|
||||
# --- notifier surface ---
|
||||
self.config = {"pushover": {}}
|
||||
self.total_torrents = 42
|
||||
self.torrent_hash_delete_list = ["a"] * 7
|
||||
self.up_tor_counter = 4
|
||||
self.ignored_counter = 5
|
||||
self.preme_tor_counter = 3
|
||||
self.tracker_protected_tag = "ipt"
|
||||
self.tracker_non_protected_tag = "public"
|
||||
self.c = Counter({"ipt": 10, "public": 20})
|
||||
self.delete_torrents = True
|
||||
self.st = datetime.datetime(2026, 10, 27, 9, 54, 0)
|
||||
self.et = datetime.datetime(2026, 10, 27, 9, 54, 12)
|
||||
self.po_key = "USERKEY"
|
||||
self.poc = StubPushover()
|
||||
self.apprise_host = "localhost"
|
||||
self.apprise_port = 8088
|
||||
self.apprise_aurls = "mailto://user:pass@example.com"
|
||||
self.tl = logging.getLogger("qbit-maid-test")
|
||||
|
||||
# --- pipeline surface ---
|
||||
self.torrent_list = []
|
||||
self.tracker_list = []
|
||||
self.qbt_client = FakeQbtClient()
|
||||
self.cat_whitelist = {}
|
||||
self.tracker_whitelist = {"tracker.example": "tracker.example"}
|
||||
self.ignored_tags = {}
|
||||
self.min_age = 432000
|
||||
self.max_age = 2419200
|
||||
self.enable_telemetry = False
|
||||
self.telemetry_outfile = "telemetry.csv"
|
||||
self.enable_dragnet = False
|
||||
self.dragnet_outfile = "orphaned.csv"
|
||||
self.cv = None
|
||||
self.use_pushover = False
|
||||
self.use_apprise = False
|
||||
self.extm = ""
|
||||
|
||||
for key, value in overrides.items():
|
||||
setattr(self, key, value)
|
||||
|
||||
def reset_counters(self):
|
||||
"""Zero the pipeline counters.
|
||||
|
||||
The constructor defaults are the notifier fixture (42 scanned, 7
|
||||
removed, ...), which pipeline tests must not inherit.
|
||||
"""
|
||||
self.up_tor_counter = 0
|
||||
self.preme_tor_counter = 0
|
||||
self.ignored_counter = 0
|
||||
self.torrent_hash_delete_list = []
|
||||
return self
|
||||
Reference in new issue
Block a user