# Developing How to test the `aur_check` package and the conventions it follows. For running the tool, see the README. ## Requirements - **Python 3.14+**, standard library only — no runtime dependencies, no install. - Built on `argparse`, `logging` (dual output: stderr + a log file), `pathlib.Path`, `urllib.request`; compressed logs via `gzip`/`lzma`/`bz2`/`compression.zstd`. - Dev tools: [ruff](https://docs.astral.sh/ruff/) (lint) and [mypy](https://mypy-lang.org/) in `strict` mode — new code must be fully typed. - Both configured in `pyproject.toml`; neither is needed to run the tool. ## Testing `unittest` + `unittest.mock`, no Arch system or `pacman`/`npm`/`bun` needed (external commands are mocked). From the repo root: ```bash python -m unittest discover -s aur_check/tests/ -t . ruff check . mypy aur_check ``` ## Conventions ### Typing & data - Modern syntax: `str | None`, never `Optional[str]`. - Result types are `@dataclass(frozen=True)`: `PackageMatch`, `LogHit`, `CacheMatch` (base for `NpmMatch`/`BunMatch`/`YarnMatch`/`PnpmMatch`), `ScanWarning`, `ScanResult`. - Line streams are `Iterator[str]` or `Generator[str, None, None]`. ### Cache matches - One `CacheMatch` aggregates *all* entries of a package in a location: `count` is the true total, `sample_paths` a bounded sample for display, and `cached_date` the most recent entry mtime. Scanners count every match (no silent cap); the aggregator keeps the sample. - `cached_date` is *when the entry was last written to the cache*, a corroborating signal for the campaign window — **not** a precise install date (re-extraction, restores, or `cp` without `-p` move it), and it is never used to filter. Label it "cached", never "installed". ### Subprocesses - Only `pacman`, `npm`, `bun`, `sort`, `zstdcat` (the last a fallback when `compression.zstd` is missing); everything else is stdlib. - Each call passes `text=True`, the default `check=False`, and a `timeout` (30s for log/package work, 15s otherwise). - Capture output with `capture_output=True`, or `input=` for the `sort` pipe. ### Defensive I/O - Keep `try`/`except` tight around I/O; never `except BaseException`. - Read text with `errors='replace'`. - Compression dispatches on the suffix: `.gz`/`.xz`/`.bz2`/`.zst` → matching decompressor, else `open(path, 'rt')`. - A failing check never stops the scan, but the error is surfaced as a `ScanWarning`, never swallowed into `[]` (which would let a scan that read nothing report `CLEAN`): `read_compressed_lines` raises, and `check_logs` records the warning and moves to the next file. ### Tests (Red/Green/Refactor) - Each module has `tests/test_.py`; classes subclass `unittest.TestCase`. - Mock each check in isolation, e.g. `@patch('aur_check.scanner.subprocess.run')`. - Assert exit codes, output strings, and date-window edge cases. ### CLI - Entry point `main(argv: list[str] | None = None) -> int` takes explicit `argv` for testability. - Returns the exit code (`0` clean, `1` warnings, `2` infected) for `sys.exit()`.