Repository navigation
Test/harden tests against mutation survivors - #10
Merged
Merged
Conversation
Mechanical formatting only, so later diffs (and the PostToolUse format hook) don't mix whitespace churn into behavior changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- tests/helpers.py: isolated_env()/scrubbed_environ() strip SKIP_RATIO_CHECK, CLEANUP_*, TRANSMISSION_* and FETCH_TORRENTS_* (except the log dir), and make_temp_dir() removes its dir on cleanup. - test_fetch_torrents applies isolated_env() module-wide, so an exported SKIP_RATIO_CHECK=true no longer fails 9 ratio-gate tests. - The stagnation watch-file test pins ft.date instead of using date.today(). - The unsafe-name watch-file test asserts on disk instead of patching the process-wide os.remove (which log rotation also calls). - Every mkdtemp/mkstemp in tests is now removed after the test. - CI runs the suite a second time with a hostile environment. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds ShouldFetchTorrentBoundaryTests: ratio exactly 1.0 vs 0.999 (standard and Devuan), only-newer / same-version-only records, highest older version deciding, other-arch isolation, Devuan ignoring non-Devuan and same-release entries, live-over-removed priority both ways, SKIP_RATIO_CHECK, unknown distro and unparseable names. Every ShouldFetchTorrent* class now pins SKIP_RATIO_CHECK=false in setUp. Hand-applied mutants listed in the task are all killed; the one survivor (dropping 'n in ratios or' in the removed-history loop) is equivalent, since setdefault() already keeps the live ratio. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Covers missing file, no markers, last-segment-wins, START without END, END before the last START, name stripping, integer ratios not matching, and duplicate names. get_previous_ratios is now fully covered by the normal unittest run; the subprocess restart test stays as-is. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
bdecode silently accepted a byte string whose declared length ran past the end of the data (b'4:ab' -> b'ab'), so a truncated .torrent decoded into a wrong info dict and hash. Missing 'e'/':' terminators and non-numeric integers/lengths leaked bare ValueErrors from bytes.index() and int(); int() also accepted '1_0' and whitespace. All of these now raise BencodeError naming the problem and its index. BencodeError still subclasses ValueError, so existing callers are unaffected. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A full mutmut 2.5.1 re-run after tasks 10-13 left four killable survivors in the touched functions: - should_fetch_torrent: 'continue' -> 'break' and 'or' -> 'and' in the removed-history skip (a skipped legacy entry first, and no parse-error warning for entries without a ratio). - bdecode: '>=' -> '>' on the end-of-data guard and the remaining-bytes arithmetic in the truncation message. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The old loop kept an end_index that was reset on every START, which made the 'end_index > start_index' check and the initial 'end_index = None' redundant - mutmut reported both as surviving (equivalent) mutants. Selecting 'after the last START, up to the last END after it' directly removes them. A new test pins the existing 'last END wins' behavior before the rewrite; mutmut now kills 28/28 mutants in the function. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Edge-value tests for plan_cleanup (ratio == min_ratio, default floor), plan_stagnation_cleanup / _stagnation_anchor (anchor exactly window_days old, delta == min_ratio_delta, oldest qualifying sample, defaults), update_ratio_history (sample interval and prune horizon edges) and plan_disk_pressure_cleanup (usage == threshold, small positive threshold). Skipped torrents (Devuan, unknown distro, unparseable, no anchor) are put before valid candidates so continue->break mutants fail, and missing/None ratios and added_dates are asserted to count as zero/oldest. A focused mutmut run kills every mutant in these seven functions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- pyproject.toml holds ruff and mypy settings so local runs and CI agree. BLE001 is ignored project-wide (broad except-and-log is the run's error-handling design); transmission_rpc gets a scoped ignore_missing_imports. - Real findings fixed: an unused variable in fetch_ubuntu_lts, the NamedTemporaryFile in evaluate_old_release_demand now uses a context manager (and also cleans up if the write fails), and the RPC payload is typed so requests' json= accepts it. - Test-side findings fixed (explicit check=False, HTTPError instead of a bare Exception, instance attribute instead of a mutable class default, unused unpacked values, combined with-statements). - ruff and mypy are pinned in requirements-dev.txt; the CI test job installs it and runs the three checks before the tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- requirements-dev.txt pins coverage and mutmut 2.5.1. - pyproject.toml configures branch coverage over the three modules and mutmut's paths/runner, so 'coverage run' and 'mutmut run' need no flags. - .gitignore covers mutmut's cache and output. - CI runs the suite under coverage and prints the report (no threshold yet; task 27 sets one). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Covers first run with credentials (exact file contents, port forwarding defaulted off), existing files keeping unrelated keys and their port forwarding setting, no-override runs leaving the file untouched, the open-whitelist warning on stderr, invalid peer limits being dropped silently (current behavior), corrupt files starting fresh, the default /config path, and running the module as a script. configure_transmission.py is now at 100% line and branch coverage. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tests/test_daily_run.py executes fetch_torrents.py as __main__ with HTTP, the Transmission client, the RPC port check, disk usage and the clock faked at their boundaries, and state/log files in a temp dir. It pins: per-outcome download counting and the summary line, distro selection, the ratio gate fed from the previous run's ratio log (blocked below 1.0, allowed at 1.0), one failing distro not stopping the others, which cleanup runs for CLEANUP_KEEP_ONLY_LATEST_VERSION, the disk-usage limit running after cleanup and its errors being logged, and seed ratios being written for the next run. No production code changed; /watch is redirected in the test because watch_dir is fixed at import time. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The ubuntu distro pattern was '^ubuntu-|^lbuntu-|^xbuntu-', but fetch_ubuntu_lts() names them lubuntu-*/xubuntu-*. get_distro() returned None for both flavours, so they bypassed the previous-version ratio gate and were never grouped for old-version/stagnation/disk-pressure cleanup. Behavior change: after this, a new Lubuntu/Xubuntu release is only fetched once the previous one reached ratio 1.0, and superseded Lubuntu/Xubuntu releases become cleanup candidates like Ubuntu's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The daily run was ~150 lines of __main__ reading os.environ, date.today() and a live Transmission client inline, and importing the module opened (and truncated) log files under /logs. Now: - configure_logging(log_dir, env) attaches the stream, rotating file and ratio handlers; import opens nothing. It replaces only its own handlers. - load_config(env) parses every env var once into a frozen Config. A bad number is recorded and raised by Config.require() where it is used, so cleanup and the disk limit still just log and skip, as before. - run(config, client_factory=, today=, now=, fetchers=, disk_usage=, download_path=) is the daily run, returning a RunSummary. - cleanup_old_versions / cleanup_stagnant_torrents take the client and today; enforce_disk_usage_limit takes a client factory, disk_usage and the download path. make_client_factory() carries the RPC credentials. The runpy characterization tests from task 17 passed unchanged against the refactor before being rewritten to call run() directly with FakeTransmission/FakeDisk; one runpy test keeps the __main__ wiring. Also adds LoadConfigTests, parse_version_type arch-variant cases (task 24) and updates CLAUDE.md's test-setup notes. fetch_torrents.py branch coverage: 91%. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds tests for bencode str encoding, missing scrape counts, UDP connect and scrape responses that are short, carry trailing bytes, an error action or another transaction's ID, distinct connect/scrape transaction IDs, the default timeout, and address resolution. Also pins the primary announce going first, the single 'announce' substitution in derive_scrape_url and scrape_torrent's default timeout. The UDP receive buffer size becomes UDP_RECV_BUFFER_BYTES with `# pragma: no mutate`: any size above the largest response read works, so no test can tell 2048 from 2049. Focused mutmut run on torrent_scrape.py: every function at 100% (string-literal-only survivors excluded). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
transmission_rpc isn't apk-only: alpine:latest (3.24) ships py3-transmission-rpc 7.0.11, which is also on PyPI. Pin that version in requirements-test.txt and drop the sys.modules MagicMock stubs: - helpers.FakeTorrent builds a real transmission_rpc.Torrent from RPC fields, so tests only read attributes the library has. DuckTorrent stays for the few tests of production's defensive None/float branches. - helpers.FakeTransmission binds each call against the real Client signature, so an argument the library wouldn't accept raises TypeError (tests/test_transmission_double.py pins this). - make_client_factory tests patch Client with autospec=True. - log_seed_ratios_via_http's request is checked against the library's RpcMethod and TORRENT_GET_ARGS tables, and its ratio ordering/None handling is pinned. - mypy now type-checks against the real library (override removed). Renaming Client's username= or remove_torrent's delete_data= in production now fails the suite. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… 22) The image was built and pushed without ever being started, and the test job only exercised pip packages on Ubuntu, never the apk ones the container runs with. - scripts/smoke_test.sh starts the image with --network none, as root and with RUN_AS_NON_ROOT=true PUID=PGID=1234. It checks entrypoint.sh got going, configure_transmission.py applied an override to settings.json, transmission-daemon is running (as uid 1234 in the non-root case), /config is owned by 1234:1234, and every module imports. About 5 s. - A Dockerfile `test` stage runs the unit suite on the image's own packages. BuildKit skips it in a normal build, and the final stage is the runtime image unchanged. - The build job runs both before any push step, so PRs get them too. Checked locally against a deliberately broken image (configure step exiting 1): the smoke test fails with the container logs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each fetcher was only tested against a hand-written ~5-line snippet of
what its page was assumed to look like, and fetch_kali_latest had no
test at all.
- scripts/record_fixtures.py captures every page the fetch_* scrapers
read into tests/fixtures/pages/<distro>/<slug>.<ext>, with the URL and
capture time in a .meta.json. Captured 2026-10-19.
- tests/test_scrapers.py replays them. Each fetcher (including both
*_old_versions) must produce exactly the candidates read off the
snapshot by hand. Also covers connection errors, HTTP 404/500, and
pages missing the element a scraper looks for.
- helpers.fake_response() returns a real requests.Response and replaces
the 19 ad-hoc FakeResponse classes, plus the one in test_daily_run.
helpers.RecordedPages serves the fixtures by URL and records any
unexpected request.
- helpers.block_network() makes every test module refuse real socket
connections.
Bug fixed: fetch_ubuntu_lts, fetch_kali_latest, fetch_kali_old_versions,
fetch_devuan_latest and fetch_linuxmint_cinnamon never checked the HTTP
status. An error page became "no releases" ({} with nothing logged, for
Ubuntu and Kali old versions) or a misleading "could not detect a
version" warning. They now call raise_for_status() and log the HTTP
error like the other fetchers.
fetch_torrents.py branch coverage: 91% -> 96%.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The merge check is deterministic, but the distro sites, the trackers and the published image (apk upgrade at start) keep moving. Nothing checked them until a user noticed missing torrents. .github/workflows/drift.yml runs on a weekly schedule and by hand, and is not part of the PR check: - scrapers: scripts/live_check.py runs every fetcher live. Each must return candidates whose names match the distro pattern with a parseable version and https .torrent URLs, at least one answering 200. On failure the pages are re-recorded and uploaded as an artifact. - tracker: scrapes a current Ubuntu torrent; counts must be integers. - transmission: scripts/transmission_roundtrip.py, run inside the published image against its own daemon. It adds a self-made tracker-less torrent via /watch, reads it like cleanup does, logs ratios over raw RPC and removes it with delete_data=True, all through fetch_torrents' own load_config/make_client_factory. - published-image: scripts/smoke_test.sh against :latest with network and apk upgrade. The script now takes SMOKE_NETWORK and SMOKE_SKIP_PACKAGE_UPDATES. - mutation: mutmut results uploaded for trend tracking; never fails. - Any failure opens, or comments on, a single issue labelled `drift`. All checks run locally on 2026-10-19 and pass. The round trip ran against an image built from this branch, since the current :latest predates load_config() and make_client_factory(). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The 3,400-line module becomes test_ratio_gate, test_fetchers, test_cleanup, test_persistence, test_logging and test_old_release_demand. The config, run-summary and client-factory tests move to test_daily_run. Shared pieces move to tests/helpers.py: DuckTorrent, fixed_date, names_of, the repo-root sys.path bootstrap (helpers.REPO_ROOT), and isolate_module(), which every module's setUpModule now calls (env scrubbing plus the network guard) in place of per-file boilerplate. Purely mechanical: the set of Class.method test IDs is identical before and after (378), and the suite and hostile-env suite pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tests/test_live_check.py (task 23) imports scripts/live_check.py, so the in-image unit suite failed to import it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…part 2) - Presence-only checks become exact values: fetch_ubuntu_lts' full candidate dict, fetch_kali_old_versions' full result, warn_if_open_without_auth's message, the cleanup parse-error warnings, the session-id request's timeout (== 15), and assertIs(..., True) for should_fetch_torrent. - test_logging's two hand-rolled RPC response classes become helpers.fake_response(); no ad-hoc response class is left in tests/. - The load/save round-trip, missing-file and corrupt-file tests, written four times (ratio history, removed history, old-release check state, settings.json), are now one JsonStoreContract mixin. It also checks that a corrupt file is reported (logger or stderr) and that saving again leaves only the file. 12 -> 16 tests. Suite: 382 tests, green; hostile-env run and in-image run also green. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Full mutmut runs (whole suite as the runner): - critical fetch_torrents logic: 70.9% -> 99.2% (256/258) - torrent_scrape: 82.6% -> 100% - configure_transmission: 89.8% -> 96.7% The two remaining critical survivors (has_unmet_demand's defaults for a missing seeder/leecher count) are now killed by test_missing_counts_mean_zero, checked by hand-applying each mutant, so 258/258. - coverage: [tool.coverage.report] fail_under = 94 (measured total 96.78% minus about 2), which CI's `coverage report` step now enforces. - tasks/README.md: targets table updated with the new numbers, every task marked done, and the remaining non-critical survivors (I/O and orchestration) listed as follow-ups. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
FakeTransmission now keeps a torrent removed without delete_data (the real Client's default) in data_left_on_disk, and FakeDisk counts it as used, so a removal only frees space the way it would for real. Before this, flipping delete_data=True to False in enforce_disk_usage_limit or cleanup_stagnant_torrents passed the whole suite. New tests pin: - delete_data=True on disk-pressure and stagnation removals - the disk-pressure entry gate at exactly the threshold, just below it, a 1% threshold, a zero-size disk, a custom download path with a single connection, the per-removal warning, and a zero ratio recorded as 0.0 - stagnation's default 30-day window and 0.02 delta, custom window_days/min_ratio_delta wiring (including the history prune), the kept/removed log lines, and a zero ratio sampled as 0.0 Kills mutants #839 #843 #844 #846 #857 #864 #868 #924 #925 #930 #940 plus an argument swap and a dropped window_days in the stagnation path. Left as equivalent: #858 (disk total turning 0 mid-loop), #861 (break vs continue once under threshold), #863/#929 (a real Torrent always has .ratio). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
check_old_releases_for_demand() was tested with evaluate_old_release_demand()
patched out, and the daily run with check_old_releases_for_demand() patched
out, so the thresholds, skip paths and counters were never really exercised:
21 mutants survived in the check, 7 in the evaluation and 2 in the run's
wiring.
- OldReleaseTracker fakes only the two network boundaries (the .torrent
download and the tracker scrape, which reads the temp file it is given),
so the real evaluation runs in between.
- One multi-candidate test covers every outcome in a single pass (removed
x2, unsafe name, already present, recently checked, below floor, zero
seeders, unknown demand, 404, wanted x2) behind a distro with no
discovery and one that finds nothing. It asserts the counts, watch files,
state, request order, log lines and that the caller's state isn't mutated.
- evaluate_old_release_demand(): the temp copy is scraped and removed even
when the scrape fails, temp-file failure, scrape timeout pass-through,
404 vs 500 logging, and the download timeout.
- Daily run: the mocked test_old_release_check_runs_only_when_enabled is
replaced by an off-by-default test and OldReleaseRunTests, which drive
the env thresholds, recheck interval, saved check state and removed
history (including SKIP_REMOVED=false) through the real check.
RunTestCase now holds the shared setUp, and _run() takes fetchers.
- test_below_absolute_threshold_is_no_demand used a zero-seeder swarm, so
it passed with the leecher floor deleted. It now uses a seeded swarm and
no ratio requirement.
All targeted mutants are killed except #715/#729 (the .get("seeders", 0)
default, which can't apply because scrape results always carry seeders).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
run() and _fetch_latest() had 14 surviving mutants: no test produced two
of the same download outcome, the clock never advanced, the downloads-folder
usage line was never asserted, and RPC credentials were checked by mocking
log_seed_ratios_via_http.
- FakeWorld.post now behaves like Transmission's RPC: a request without the
session id gets a 409 carrying it, and the torrent-get records its auth.
FakeWorld.disk_usage records the path it was asked about.
- The credentials test asserts the auth the RPC endpoint actually received,
plus a no-credentials case, instead of mocking the internal function.
- New run tests:
- two of every outcome (added, existing, not found, failed, and fetchers
that found nothing), with a removed-history ratio-blocked candidate
first that must not stop the rest
- low-demand images skipped by default and fetched with
FETCH_TORRENTS_INCLUDE_LOW_DEMAND=true
- elapsed time from a clock that advances
- the exact "Downloads folder usage" line and the /downloads path
- _run() also takes `now`.
All 17 targeted mutants in run()/_fetch_latest() are killed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
configure_logging() was tested only for which handlers it builds. No record
was ever sent through them, so the [ratio] split, FETCH_TORRENTS_ALWAYS_LOG,
the log level and the formatter were unasserted. parse_log_level() had no
direct test, and the always-log prefixes were a silent string contract with
the run's message wording. 17 mutants survived across these.
- ParseLogLevelTests: unset, blank, digits, names (case and whitespace),
unknown, and a non-int digit ('²') falling back to the default.
- LogRoutingTests emit debug/info/warning, an always-log progress line and
[ratio] lines, then assert exactly what lands in stderr,
fetch_torrents.log and the ratio log for:
- the defaults
- a higher FETCH_TORRENTS_LOG_LEVEL
- ALWAYS_LOG off with the LOG_LEVEL fallback
- FETCH_TORRENTS_LOG_LEVEL beating LOG_LEVEL
It also checks the timestamped format and that the logger doesn't
propagate.
- A run-level test checks every get_always_log_prefixes() entry matches a
message the daily run really emits, so rewording one now fails.
- test_still_starts_with_run_complete_for_always_log_filter never touched
the filter. Renamed to test_is_let_through_by_the_always_log_filter, it
now runs the summary through ImportantMessageFilter at ERROR level.
All targeted mutants are killed except #82 (propagate = None), which
logging treats the same as False.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The 2026-10-19 test-health review's top five items are done (90c7f8f, db38abe, b4d010a, aa81ef3). The rest of its findings become tasks 28-38: production seams for the RPC wait, UDP transaction ids and live_check; atomic .torrent writes; scrape_torrent fall-through and SSRF edges; configure_transmission gaps; the remaining mutation survivors; mechanical tidy-up; fixture consolidation; toolchain hardening; and a final re-measure with a coverage ratchet. The README records the new baseline at aa81ef3: 423 tests, 99% branch coverage, fetch_torrents.py mutation 95.4% (1027/1076). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
download_torrent() and the old-release add wrote straight to /watch/<name>.torrent, so a write that died part-way left a partial file that counted as "existing" forever. Both now write a dot-prefixed .<name>.torrent.part (outside transmission's *.torrent glob) and os.replace() it into place. A failed old-release write is logged, skipped and left unrecorded so the next run retries it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
wait_for_transmission_rpc() now takes connect/clock/sleep seams and uses time.monotonic() rather than wall-clock time. run() takes the readiness check as wait_for_rpc, so the daily-run tests drive the "RPC unavailable" path through it instead of patching both the function and socket.create_connection. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
is_reachable(), check_scrapers(), check_tracker() and main() take their collaborators (HEAD/GET, fetchers, reachability, scrape, the checks map) as defaulted arguments, so the drift job's HEAD-405 fallback, MAY_BE_EMPTY, "one reachable is enough", the tracker for/else and integer validation, and exit codes 0/1/2 are all tested offline. The test module now loads live_check from its path instead of putting scripts/ on sys.path, and fake_response() marks its body consumed so a response can be used as a context manager like a real one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A parametrised test has tracker A fail by connection error, HTTP status, failure reason, missing stats or a non-scrapable announce URL, and checks B's exact %-encoded request URL, timeout and whole result. UDP calls are asserted with exact (host, port, info_hash, timeout). SSRF: an empty resolution is unsafe; multicast, unspecified and IPv4-mapped internal addresses are pinned as disallowed (all already were, so no production change). Hand-built Mock responses are replaced by tracker_response() over helpers.fake_response, and the duplicated resolver fakes by resolve_private(). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ff S, pins (task 37) - smoke_test.sh now waits for "Starting torrent fetch run." in /logs/fetch_torrents.log in both the root and RUN_AS_NON_ROOT runs, so a fetch script that crashes on import fails the build. - The test job runs shellcheck v0.11.0 and hadolint v2.15.1 (pinned images). The two Dockerfile ignores (rolling alpine:latest, unpinned apk packages) are justified inline. - ruff adds the bandit S family. The info hash uses sha1(usedforsecurity=False); test-only ignores are justified in pyproject.toml. - CI Python is pinned to alpine:latest's 3.14, and requests and beautifulsoup4 to the apk versions (2.33.1, 4.15.0). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
New tests pin the 30 s timeout on every fetcher and download, Arch/Kali/ Fedora selection and ordering, the unknown-distro warning text, cleanup_old_versions' default floor and zero-ratio record, ratio rounding, the credential fallback needing both halves, parse_bool's default, RunSummary/Config defaults and immutability, the state-file paths, and the state files' indented, sorted JSON. Bug fix: a Fedora listing that links the newest torrent twice no longer reports the duplicate as an old release. Both Fedora fetchers now share one de-duplicating parser. Arch's max() and the disk-usage percentage were simplified so their equivalent mutants are no longer generated; the remaining accepted equivalents are listed in tasks/README.md. Also pins types-requests: requests 2.33 (the apk pin from task 37) ships no inline types, so mypy failed without it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… 29) scrape_udp_tracker() takes new_transaction_id (default _random_transaction_id), so tests pass a fixed id sequence instead of patching os.urandom process-wide. The UDP tests use an autospec'd socket and assert the connect and scrape datagrams byte for byte. The test that guards the fresh scrape id now says so. Every bare assertRaises in test_torrent_scrape.py now checks a message fragment, and the duplicate short-response tests are folded together. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lts (task 31) main(settings_path=None) falls back to TRANSMISSION_SETTINGS_FILE, then the new DEFAULT_SETTINGS_FILE constant. The default-path test patches that constant, so it no longer depends on whether this machine has a real /config/settings.json. New tests pin a peer limit of exactly 1, case/whitespace-insensitive TRANSMISSION_PORT_FORWARDING, and apply_overrides' whole result with its input unchanged. Each subTest case gets its own temp dir. The settings.json layout is recorded as an accepted equivalent. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…sts (task 34) Expected values that came from production (ft._kali_torrent_urls, ft.DEFAULT_DISTROS, ft.ALL_DISTROS) are now written out: the Kali image list moved to helpers.kali_candidates(), shared with test_scrapers.py. Config tests assert only the project's "ENV_VAR: " prefix, not CPython's float()/int() error text. The import subprocess runs with -W ignore. Count-only and startswith/any() asserts now check names, exact log lines and that nothing was removed (new FakeTransmission.removals). parse_supported_distros' unknown-entry and fallback cases assert their warnings. The plan_cleanup per-distro test is a subTest table plus a combined run. ScraperTestCase.fetch() returns a Fetched result instead of stashing self.pages and logging a dummy line; FailurePathTests checks that one error naming the cause was logged, merges 404 into one HTTP status test and renames LATEST_FETCHERS to SINGLE_PAGE_FETCHERS. Removed duplicates of stricter tests: the Devuan no-link, Arch no-releases and Kali single-version fetcher tests, the session-id timeout test, and the second fallback warning test. Also removed the duplicate __main__ block in test_cleanup.py and the unused helpers.use_isolated_env. helpers.fixed_date now backs a test that check_old_releases_for_demand() defaults today to date.today(); the other old-release tests pass today explicitly. Moved: config, parse_bool, client factory, run summary and value-type tests to test_config.py; test_transmission_double.py plus NetworkGuardTests to test_helpers.py; get_distro and the lubuntu ratio gate out of ParseVersionTypeTests; distro_of into its own class. CLAUDE.md lists the new modules. Only one _rpc_port_open stub was left, so there was nothing to hoist. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…task 35) watch_dir is now FETCH_TORRENTS_WATCH_DIR (default /watch), read once at import like log_dir, and documented in the README. Tests check the default and the override in a fresh interpreter. MainWiringTests points it, and FETCH_TORRENTS_LOG_DIR, at FakeWorld's temp dirs, and now routes Arch so the runpy run really writes a torrent there. get_rpc_credentials() is gone. load_config() and log_seed_ratios_via_http()'s env fallback both use _rpc_credentials(env), and Config.rpc_auth is the only rule for when both halves count. A test pins the fallback to Config.rpc_auth. New helpers: use_watch_dir(), use_state_files() (both return the temp paths) and removed(name, ratio=None). Also FakeTransmissionRpc, the one strict HTTP RPC fake: 409 plus session id, auth recorded, method and fields checked against the pinned transmission_rpc. test_logging and FakeWorld both use it in place of their own handshakes. All of these are tested in test_helpers.py, and removed() is checked against record_removal(). Every setUp that patched watch_dir or a state-file global now calls the helpers. test_old_release_demand gets demand(scrape, **overrides) with the daily run's default thresholds. FakeWorld gets hold_current_and_superseded_ubuntu(). The ratio-key tests are one table, which adds the missing Kali and Arch high-ratio rows. CLAUDE.md documents the new helpers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…o 97 The full mutmut run for task 38 found three new behavioural survivors: - fetch_kali_latest's "if not results" branch was unreachable (_kali_torrent_urls always returns its fixed list), so it is removed. - The old-release write-failure branch from task 32 now has a test where a skip comes before the failed write and a later candidate still gets added, which kills "skipped = 1" and "continue -> break". Coverage is 99.13% (branch), so fail_under goes from 94 to 97. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Full mutmut 2.5.1 runs at 209292c: fetch_torrents.py 99.3% (1040/1047), torrent_scrape.py 99.6% (248/249), configure_transmission.py 96.8% (90/93), critical set still 100%. Every behavioural survivor left is an accepted equivalent, now including the FIPS-only usedforsecurity flag. Branch coverage is 99.1% against the new fail_under = 97. The mutmut docs note the stale-.pyc and Windows-encoding pitfalls hit here. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fetch_torrents.py and configure_transmission.py are run as scripts, so record them as 100755 in git. torrent_scrape.py is only imported, so its shebang goes instead. Ruff on Windows can't see the exec bit, which is why this only failed on the Linux runner. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ruff's EXE001 only sees the exec bit on Linux, so a 100644 shebang file passes lint on Windows and fails on CI. test_repo_hygiene reads modes from the git index, which works on every OS. It also caught entrypoint.sh, which the Dockerfile chmods but git stored as 100644. Both CLAUDE.md files note the rule and the update-index fix. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
run() calls _check_old_releases() unguarded, so with FETCH_TORRENTS_CHECK_OLD_RELEASES=true one invalid FETCH_TORRENTS_OLD_RELEASE_* value aborts every daily run before ratio logging, cleanup and the disk-usage limit. Found while reviewing this branch's production changes; the behavior predates the branch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.