turnstone/tests/test_ingest_qbittorrent.py
pyr0ball d05430ef85 feat: qBittorrent log ingestor with 8 diagnostic patterns
Adds app/ingest/qbittorrent.py — auto-detected by the pipeline on the
(YYYY/MM/DD HH:MM:SS) timestamp fingerprint. Handles both slash and dash
date separators, optional [Warning|Critical] bracket levels, and
multi-line continuations (Qt stack traces).

patterns/default.yaml: 8 new qbit_ patterns covering tracker errors,
port bind failures, disk errors, hash check failures, peer bans, download
completion, ratio limits, and session errors.

manage.sh: ingest-qbit [HOST] command mirrors ingest-plex — probes known
default log paths locally or via SSH, ingests, restarts server.

14 tests covering format detection, severity mapping, multiline handling,
and timestamp normalization.
2026-05-10 08:21:16 -07:00

90 lines
3.3 KiB
Python

"""Tests for the qBittorrent log ingestor."""
from __future__ import annotations
import pytest
from app.ingest.qbittorrent import is_qbit_log, parse
SAMPLE_LOG = """\
(2026/05/09 14:10:01) qBittorrent v5.0.3 started
(2026/05/09 14:10:02) [Warning] Tracker 'http://tracker.example.com/announce' is not working. Reason: Connection timed out
(2026/05/09 14:10:03) [Critical] Couldn't listen on any of the network interfaces. Aborting!
(2026/05/09 14:10:04) Download of 'ubuntu-24.04.iso' has finished.
(2026/05/09 14:10:05) [Warning] Hash check failed for piece 42 of 'ubuntu-24.04.iso'
(2026/05/09 14:10:06) Some long message
that continues on the next line
and a third line
(2026/05/09 14:10:07) Normal message without bracket level
"""
DASH_FORMAT = "(2026-05-09 14:10:01) qBittorrent v4.6.2 started\n"
class TestDetector:
def test_detects_slash_format(self):
assert is_qbit_log("(2026/05/09 14:10:01) qBittorrent started")
def test_detects_dash_format(self):
assert is_qbit_log(DASH_FORMAT.strip())
def test_rejects_plex_format(self):
assert not is_qbit_log("Jan 01, 2026 12:00:00.000 [12345] DEBUG - message")
def test_rejects_journald_json(self):
assert not is_qbit_log('{"__REALTIME_TIMESTAMP": "12345", "MESSAGE": "hi"}')
def test_rejects_plaintext(self):
assert not is_qbit_log("2026-05-09 14:10:01 some syslog line")
class TestParser:
def _parse(self, text: str) -> list:
return list(parse(iter(text.splitlines(keepends=True)), "qbit_test", []))
def test_entry_count(self):
entries = self._parse(SAMPLE_LOG)
assert len(entries) == 7
def test_startup_entry(self):
e = self._parse(SAMPLE_LOG)[0]
assert "qBittorrent v5.0.3 started" in e.text
# No bracket level + no severity keyword in text → None (consistent with other ingestors)
assert e.severity is None
assert e.timestamp_iso == "2026-05-09T14:10:01+00:00"
def test_warning_severity(self):
entries = self._parse(SAMPLE_LOG)
tracker_entry = entries[1]
assert tracker_entry.severity == "WARN"
assert "not working" in tracker_entry.text
def test_critical_severity(self):
entries = self._parse(SAMPLE_LOG)
port_entry = entries[2]
assert port_entry.severity == "CRITICAL"
def test_multiline_continuation(self):
entries = self._parse(SAMPLE_LOG)
multiline = entries[5]
assert "continues on the next line" in multiline.text
assert "third line" in multiline.text
def test_no_level_bracket_falls_back_to_detect(self):
entries = self._parse(SAMPLE_LOG)
last = entries[6]
assert last.text == "Normal message without bracket level"
def test_source_id_propagated(self):
entries = self._parse(SAMPLE_LOG)
assert all(e.source_id == "qbit_test" for e in entries)
def test_sequence_is_monotonic(self):
entries = self._parse(SAMPLE_LOG)
sequences = [e.sequence for e in entries]
assert sequences == sorted(sequences)
assert len(set(sequences)) == len(sequences)
def test_dash_format_timestamp(self):
entries = list(parse(iter(DASH_FORMAT.splitlines(keepends=True)), "qbit", []))
assert len(entries) == 1
assert entries[0].timestamp_iso == "2026-05-09T14:10:01+00:00"