diff --git a/.coverage b/.coverage new file mode 100644 index 0000000..5744558 Binary files /dev/null and b/.coverage differ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8fdf573..b87bec4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,8 +22,84 @@ jobs: nix_path: nixpkgs=channel:nixos-25.05 extra_nix_config: | experimental-features = nix-command flakes - - name: Build Rust module and run tests via Nix - run: nix develop --command bash -lc 'just venv ${{matrix.python-version}} dev test' + - name: Prepare dev environment + run: nix develop --command bash -lc 'just venv ${{matrix.python-version}} dev' + + - name: Rust tests + run: nix develop --command bash -lc 'just cargo-test' + + - name: Python tests + run: nix develop --command bash -lc 'just py-test' + + coverage: + name: Coverage (Python 3.12) + needs: nix-tests + runs-on: ubuntu-latest + continue-on-error: true + permissions: + contents: read + issues: write + pull-requests: write + steps: + - uses: actions/checkout@v4 + - uses: cachix/install-nix-action@v27 + with: + nix_path: nixpkgs=channel:nixos-25.05 + extra_nix_config: | + experimental-features = nix-command flakes + + - name: Prepare dev environment (Python 3.12) + run: nix develop --command bash -lc 'just venv 3.12 dev' + + - name: Collect coverage + id: coverage-run + run: nix develop --command bash -lc 'just coverage' + + - name: Generate coverage comment + if: steps.coverage-run.outcome == 'success' + run: | + ROOT="$(pwd)" + nix develop --command bash -lc "python3 codetracer-python-recorder/scripts/generate_coverage_comment.py \ + --rust-summary codetracer-python-recorder/target/coverage/rust/summary.json \ + --python-json codetracer-python-recorder/target/coverage/python/coverage.json \ + --output codetracer-python-recorder/target/coverage/coverage-comment.md \ + --repo-root \"${ROOT}\"" + + - name: Upload Rust coverage artefacts + if: steps.coverage-run.outcome == 'success' + uses: actions/upload-artifact@v4 + with: + name: rust-coverage + path: | + codetracer-python-recorder/target/coverage/rust/lcov.info + codetracer-python-recorder/target/coverage/rust/summary.json + if-no-files-found: warn + + - name: Upload Python coverage artefacts + if: steps.coverage-run.outcome == 'success' + uses: actions/upload-artifact@v4 + with: + name: python-coverage + path: | + codetracer-python-recorder/target/coverage/python/coverage.xml + codetracer-python-recorder/target/coverage/python/coverage.json + if-no-files-found: warn + + - name: Upload coverage comment + if: steps.coverage-run.outcome == 'success' + uses: actions/upload-artifact@v4 + with: + name: coverage-comment + path: codetracer-python-recorder/target/coverage/coverage-comment.md + if-no-files-found: warn + + - name: Comment coverage summary + if: github.event_name == 'pull_request' && steps.coverage-run.outcome == 'success' + uses: peter-evans/create-or-update-comment@v4 + with: + issue-number: ${{ github.event.pull_request.number }} + identifier: coverage-summary + body-path: codetracer-python-recorder/target/coverage/coverage-comment.md # rust-tests: # name: Rust module test on ${{ matrix.os }} (Python ${{ matrix.python-version }}) diff --git a/Justfile b/Justfile index a5c15e4..32a8c23 100644 --- a/Justfile +++ b/Justfile @@ -41,12 +41,30 @@ cargo-test: uv run cargo nextest run --manifest-path codetracer-python-recorder/Cargo.toml --no-default-features py-test: - uv run --group dev --group test pytest + uv run --group dev --group test pytest codetracer-python-recorder/tests/python codetracer-pure-python-recorder # Run tests only on the pure recorder test-pure: uv run --group dev --group test pytest codetracer-pure-python-recorder +# Generate combined coverage artefacts for both crates +coverage: + just coverage-rust + just coverage-python + +coverage-rust: + mkdir -p codetracer-python-recorder/target/coverage/rust + LLVM_COV="$(command -v llvm-cov)" LLVM_PROFDATA="$(command -v llvm-profdata)" \ + uv run cargo llvm-cov nextest --manifest-path codetracer-python-recorder/Cargo.toml --no-default-features --lcov --output-path codetracer-python-recorder/target/coverage/rust/lcov.info + LLVM_COV="$(command -v llvm-cov)" LLVM_PROFDATA="$(command -v llvm-profdata)" \ + uv run cargo llvm-cov report --summary-only --json --manifest-path codetracer-python-recorder/Cargo.toml --output-path codetracer-python-recorder/target/coverage/rust/summary.json + python3 codetracer-python-recorder/scripts/render_rust_coverage_summary.py \ + codetracer-python-recorder/target/coverage/rust/summary.json --root "$(pwd)" + +coverage-python: + mkdir -p codetracer-python-recorder/target/coverage/python + uv run --group dev --group test pytest --cov=codetracer_python_recorder --cov-report=term --cov-report=xml:codetracer-python-recorder/target/coverage/python/coverage.xml --cov-report=json:codetracer-python-recorder/target/coverage/python/coverage.json codetracer-python-recorder/tests/python + # Build the module in release mode build: just venv \ @@ -64,4 +82,3 @@ test-all: file="${file[0]}"; \ uv run -p "python3.$v" --with "${file}" --with pytest -- pytest -q; \ done - diff --git a/README.md b/README.md index 9dfa927..778a833 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,15 @@ Basic workflow: - from codetracer_python_recorder import hello - hello() +#### Testing & Coverage + +- Run the full split test suite (Rust nextest + Python pytest): `just test` +- Run only Rust integration/unit tests: `just cargo-test` +- Run only Python tests (including the pure-Python recorder to guard regressions): `just py-test` +- Collect coverage artefacts locally (LCOV + Cobertura/JSON): `just coverage` + +The CI workflow mirrors these commands. Pull requests get an automated comment with the latest Rust/Python coverage tables and downloadable artefacts (`lcov.info`, `coverage.xml`, `coverage.json`). + ### Future directions The current Python support is an unfinished prototype. We can finish it. In the future, it may be expanded to function in a way to similar to the more complete implementations, e.g. [Noir](https://github.com/blocksense-network/noir/tree/blocksense/tooling/tracer). diff --git a/codetracer-python-recorder/scripts/generate_coverage_comment.py b/codetracer-python-recorder/scripts/generate_coverage_comment.py new file mode 100755 index 0000000..82bab17 --- /dev/null +++ b/codetracer-python-recorder/scripts/generate_coverage_comment.py @@ -0,0 +1,191 @@ +#!/usr/bin/env python3 +"""Generate a Markdown coverage summary comment for CI.""" + +from __future__ import annotations + +import argparse +import json +import pathlib +from typing import Dict, Iterable, List, Tuple + +from render_rust_coverage_summary import load_summary as load_rust_summary + +Row = Tuple[str, int, int, float] + + +def parse_args(argv: Iterable[str] | None = None) -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__ or "") + parser.add_argument( + "--rust-summary", + type=pathlib.Path, + required=True, + help="Path to cargo-llvm-cov JSON summary", + ) + parser.add_argument( + "--python-json", + type=pathlib.Path, + required=True, + help="Path to coverage.py JSON report", + ) + parser.add_argument( + "--output", + type=pathlib.Path, + required=True, + help="Output Markdown file for the PR comment", + ) + parser.add_argument( + "--repo-root", + type=pathlib.Path, + default=pathlib.Path.cwd(), + help="Repository root used to relativise file paths (default: current working directory)", + ) + parser.add_argument( + "--max-rows", + type=int, + default=20, + help="Maximum number of per-file rows to display for each language (default: 20).", + ) + return parser.parse_args(argv) + + +def _select_rows(rows: List[Row], max_rows: int) -> Tuple[List[Row], bool]: + if max_rows <= 0 or len(rows) <= max_rows: + return sorted(rows, key=lambda item: item[0]), False + + priority_sorted = sorted(rows, key=lambda item: (-item[2], item[0])) + trimmed = priority_sorted[:max_rows] + return sorted(trimmed, key=lambda item: item[0]), True + + +def _format_table(rows: List[Row], headers: Tuple[str, str, str, str]) -> List[str]: + lines = [ + f"| {headers[0]} | {headers[1]} | {headers[2]} | {headers[3]} |", + "| --- | ---: | ---: | ---: |", + ] + for name, total, missed, percent in rows: + lines.append( + f"| `{name}` | {total:,} | {missed:,} | {percent:5.1f}% |" + ) + return lines + + +def _load_python_rows( + report_path: pathlib.Path, + repo_root: pathlib.Path, +) -> Tuple[List[Row], Dict[str, float]]: + try: + payload = json.loads(report_path.read_text(encoding="utf-8")) + except FileNotFoundError as exc: + raise SystemExit(f"Python coverage JSON not found: {report_path}") from exc + + repo_root = repo_root.resolve() + rows: List[Row] = [] + + for path_str, details in payload.get("files", {}).items(): + summary = (details.get("summary") or {}) + total = int(summary.get("num_statements", 0)) + missed = int(summary.get("missing_lines", 0)) + percent = float(summary.get("percent_covered", 0.0)) + + if total == 0 and missed == 0: + continue + + file_path = pathlib.Path(path_str) + if not file_path.is_absolute(): + file_path = (repo_root / file_path).resolve() + else: + file_path = file_path.resolve() + + try: + rel_path = file_path.relative_to(repo_root) + except ValueError: + continue + + rows.append((rel_path.as_posix(), total, missed, percent)) + + rows.sort(key=lambda item: item[0]) + + totals = payload.get("totals", {}) + return rows, { + "total": float(totals.get("num_statements", 0)), + "covered": float(totals.get("covered_lines", 0)), + "missed": float(totals.get("missing_lines", 0)), + "percent": float(totals.get("percent_covered", 0.0)), + } + + +def _load_rust_rows( + summary_path: pathlib.Path, + repo_root: pathlib.Path, +) -> Tuple[List[Row], Dict[str, float]]: + rows, totals = load_rust_summary(summary_path, repo_root) + # Normalise totals dict to expected keys + total = float(totals.get("count", 0)) + covered = float(totals.get("covered", 0)) + missed = float(totals.get("notcovered", total - covered)) + return rows, { + "total": total, + "covered": covered, + "missed": missed, + "percent": float(totals.get("percent", 0.0)), + } + + +def _format_summary_block( + heading: str, + column_label: str, + totals: Dict[str, float], + rows: List[Row], + max_rows: int, +) -> List[str]: + display_rows, truncated = _select_rows(rows, max_rows) + lines = [heading] + total = int(totals.get("total", 0)) + covered = int(totals.get("covered", 0)) + missed = int(totals.get("missed", total - covered)) + percent = totals.get("percent", 0.0) + lines.append( + f"**{percent:0.1f}%** covered ({covered:,} / {total:,} | {missed:,} missed)" + ) + lines.extend( + _format_table(display_rows, ("File", column_label, "Miss", "Cover")) + ) + if truncated: + lines.append( + f"_Showing top {max_rows} entries by missed lines (of {len(rows)} total)._" + ) + return lines + + +def main(argv: Iterable[str] | None = None) -> int: + args = parse_args(argv) + repo_root = args.repo_root.resolve() + + rust_rows, rust_totals = _load_rust_rows(args.rust_summary, repo_root) + python_rows, python_totals = _load_python_rows(args.python_json, repo_root) + + output_lines: List[str] = ["### Coverage Summary", ""] + + output_lines.extend( + _format_summary_block( + "**Rust (lines)**", "Lines", rust_totals, rust_rows, args.max_rows + ) + ) + output_lines.extend(["", ""]) + output_lines.extend( + _format_summary_block( + "**Python (statements)**", "Stmts", python_totals, python_rows, args.max_rows + ) + ) + output_lines.append("") + output_lines.append( + "_Generated automatically via `generate_coverage_comment.py`._" + ) + + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text("\n".join(output_lines) + "\n", encoding="utf-8") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/codetracer-python-recorder/scripts/render_rust_coverage_summary.py b/codetracer-python-recorder/scripts/render_rust_coverage_summary.py new file mode 100755 index 0000000..cb18ab4 --- /dev/null +++ b/codetracer-python-recorder/scripts/render_rust_coverage_summary.py @@ -0,0 +1,102 @@ +"""Render a concise Rust coverage summary table from cargo-llvm-cov JSON.""" + +from __future__ import annotations + +import argparse +import json +import pathlib +import sys +from typing import Dict, Iterable, List, Tuple + + +def _load_payload(summary_path: pathlib.Path) -> Dict: + try: + return json.loads(summary_path.read_text(encoding="utf-8")) + except FileNotFoundError as exc: + raise SystemExit(f"Rust coverage summary not found: {summary_path}") from exc + + +def parse_args(argv: Iterable[str] | None = None) -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__ or "") + parser.add_argument( + "summary_path", + type=pathlib.Path, + help="Path to cargo-llvm-cov JSON summary (e.g. summary.json)", + ) + parser.add_argument( + "--root", + type=pathlib.Path, + default=pathlib.Path.cwd(), + help="Repository root used to relativise file paths (default: current working directory)", + ) + return parser.parse_args(argv) + + +def load_rows(summary_path: pathlib.Path, repo_root: pathlib.Path) -> List[Tuple[str, int, int, float]]: + payload = _load_payload(summary_path) + rows, _ = load_summary(summary_path, repo_root, payload) + return rows + + +def load_summary( + summary_path: pathlib.Path, + repo_root: pathlib.Path, + payload: Dict | None = None, +) -> Tuple[List[Tuple[str, int, int, float]], Dict[str, float]]: + if payload is None: + payload = _load_payload(summary_path) + + repo_root = repo_root.resolve() + rows: List[Tuple[str, int, int, float]] = [] + + totals: Dict[str, float] = {} + + for dataset in payload.get("data", []): + dataset_totals = dataset.get("totals", {}) + if dataset_totals: + totals = dataset_totals.get("lines", dataset_totals) + for entry in dataset.get("files", []): + filename = entry.get("filename") + if not filename: + continue + path = pathlib.Path(filename) + try: + rel_path = path.resolve().relative_to(repo_root) + except Exception: + # Skip entries outside the repository (stdlib, third-party deps, etc.). + continue + + line_summary = (entry.get("summary") or {}).get("lines") or {} + total = int(line_summary.get("count", 0)) + covered = int(line_summary.get("covered", 0)) + missed = max(total - covered, 0) + percent = float(line_summary.get("percent", 0.0)) + rows.append((rel_path.as_posix(), total, missed, percent)) + + rows.sort(key=lambda item: item[0]) + return rows, totals + + +def render(rows: List[Tuple[str, int, int, float]]) -> str: + if not rows: + return "Rust coverage summary: no project files found" + + name_width = max(len(name) for name, *_ in rows) + lines = ["Rust coverage summary (lines):", f"{'Name'.ljust(name_width)} Lines Miss Cover"] + + for name, total, missed, percent in rows: + lines.append(f"{name.ljust(name_width)} {total:5d} {missed:4d} {percent:5.1f}%") + + return "\n".join(lines) + + +def main(argv: Iterable[str] | None = None) -> int: + args = parse_args(argv) + rows, _ = load_summary(args.summary_path, args.root) + output = render(rows) + print(output) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/codetracer-python-recorder/src/runtime/activation.rs b/codetracer-python-recorder/src/runtime/activation.rs index 43ac88e..44f85a1 100644 --- a/codetracer-python-recorder/src/runtime/activation.rs +++ b/codetracer-python-recorder/src/runtime/activation.rs @@ -85,6 +85,82 @@ impl ActivationController { } } +#[cfg(test)] +mod tests { + use super::*; + use pyo3::types::{PyAnyMethods, PyCode, PyModule}; + use pyo3::{Bound, Python}; + use std::ffi::CString; + + fn build_code(py: Python<'_>, name: &str, filename: &str) -> CodeObjectWrapper { + let module_src = format!("def {name}():\n return 42\n"); + let c_src = CString::new(module_src).expect("source"); + let c_filename = CString::new(filename).expect("filename"); + let c_module = CString::new("m").expect("module"); + let module = PyModule::from_code( + py, + c_src.as_c_str(), + c_filename.as_c_str(), + c_module.as_c_str(), + ) + .expect("compile module"); + let func = module.getattr(name).expect("fetch function"); + let code: Bound<'_, PyCode> = func + .getattr("__code__") + .expect("__code__") + .downcast_into() + .expect("code"); + CodeObjectWrapper::new(py, &code) + } + + #[test] + fn starts_active_when_no_activation_path() { + let controller = ActivationController::new(None); + assert!(controller.is_active()); + } + + #[test] + fn remains_inactive_until_activation_code_runs() { + Python::with_gil(|py| { + let code = build_code(py, "target", "/tmp/target.py"); + let mut controller = ActivationController::new(Some(Path::new("/tmp/target.py"))); + assert!(!controller.is_active()); + assert!(controller.should_process_event(py, &code)); + assert!(controller.is_active()); + }); + } + + #[test] + fn ignores_non_matching_code_objects() { + Python::with_gil(|py| { + let code = build_code(py, "other", "/tmp/other.py"); + let mut controller = ActivationController::new(Some(Path::new("/tmp/target.py"))); + assert!(!controller.should_process_event(py, &code)); + assert!(!controller.is_active()); + }); + } + + #[test] + fn deactivates_after_activation_return() { + Python::with_gil(|py| { + let code = build_code(py, "target", "/tmp/target.py"); + let mut controller = ActivationController::new(Some(Path::new("/tmp/target.py"))); + assert!(controller.should_process_event(py, &code)); + assert!(controller.is_active()); + assert!(controller.handle_return_event(code.id())); + assert!(!controller.is_active()); + assert!(!controller.should_process_event(py, &code)); + }); + } + + #[test] + fn start_path_prefers_activation_path() { + let controller = ActivationController::new(Some(Path::new("/tmp/target.py"))); + let fallback = Path::new("/tmp/fallback.py"); + assert_eq!(controller.start_path(fallback), Path::new("/tmp/target.py")); + } +} + impl ActivationController { #[allow(dead_code)] pub fn handle_return(&mut self, code_id: usize) -> bool { diff --git a/codetracer-python-recorder/src/runtime/output_paths.rs b/codetracer-python-recorder/src/runtime/output_paths.rs index b6b3f82..088f932 100644 --- a/codetracer-python-recorder/src/runtime/output_paths.rs +++ b/codetracer-python-recorder/src/runtime/output_paths.rs @@ -58,3 +58,62 @@ impl TraceOutputPaths { Ok(()) } } + +#[cfg(test)] +mod tests { + use super::*; + use runtime_tracing::{Line, TraceLowLevelEvent}; + use tempfile::tempdir; + + #[test] + fn json_paths_use_json_filenames() { + let tmp = tempdir().expect("tempdir"); + let paths = TraceOutputPaths::new(tmp.path(), TraceEventsFileFormat::Json); + assert_eq!(paths.events(), tmp.path().join("trace.json").as_path()); + assert_eq!( + paths.metadata(), + tmp.path().join("trace_metadata.json").as_path() + ); + assert_eq!(paths.paths(), tmp.path().join("trace_paths.json").as_path()); + } + + #[test] + fn binary_paths_use_bin_extension() { + let tmp = tempdir().expect("tempdir"); + let paths = TraceOutputPaths::new(tmp.path(), TraceEventsFileFormat::BinaryV0); + assert_eq!(paths.events(), tmp.path().join("trace.bin").as_path()); + } + + #[test] + fn configure_writer_initialises_writer_state() { + let tmp = tempdir().expect("tempdir"); + let start_path = tmp.path().join("program.py"); + std::fs::write(&start_path, "print('hi')\n").expect("write script"); + + let paths = TraceOutputPaths::new(tmp.path(), TraceEventsFileFormat::Json); + let mut writer = NonStreamingTraceWriter::new("program.py", &[]); + + paths + .configure_writer(&mut writer, &start_path, 123) + .expect("configure writer"); + + let recorded_path = writer.events.iter().find_map(|event| match event { + TraceLowLevelEvent::Path(p) => Some(p.clone()), + _ => None, + }); + assert_eq!(recorded_path.as_deref(), Some(start_path.as_path())); + + let function_record = writer.events.iter().find_map(|event| match event { + TraceLowLevelEvent::Function(record) => Some(record.clone()), + _ => None, + }); + let record = function_record.expect("function record"); + assert_eq!(record.line, Line(123)); + + let has_call = writer + .events + .iter() + .any(|event| matches!(event, TraceLowLevelEvent::Call(_))); + assert!(has_call, "expected toplevel call event"); + } +} diff --git a/codetracer-python-recorder/src/session/bootstrap.rs b/codetracer-python-recorder/src/session/bootstrap.rs index ea257ba..1327f82 100644 --- a/codetracer-python-recorder/src/session/bootstrap.rs +++ b/codetracer-python-recorder/src/session/bootstrap.rs @@ -119,3 +119,121 @@ pub fn collect_program_metadata(py: Python<'_>) -> PyResult { Ok(ProgramMetadata { program, args }) } + +#[cfg(test)] +mod tests { + use super::*; + use pyo3::types::PyList; + use tempfile::tempdir; + + #[test] + fn ensure_trace_directory_creates_missing_dir() { + let tmp = tempdir().expect("tempdir"); + let target = tmp.path().join("trace-out"); + ensure_trace_directory(&target).expect("create directory"); + assert!(target.is_dir()); + } + + #[test] + fn ensure_trace_directory_rejects_file_path() { + let tmp = tempdir().expect("tempdir"); + let file_path = tmp.path().join("trace.bin"); + std::fs::write(&file_path, b"stub").expect("write stub file"); + assert!(ensure_trace_directory(&file_path).is_err()); + } + + #[test] + fn resolve_trace_format_accepts_supported_aliases() { + assert!(matches!( + resolve_trace_format("json").expect("json format"), + TraceEventsFileFormat::Json + )); + assert!(matches!( + resolve_trace_format("BiNaRy").expect("binary alias"), + TraceEventsFileFormat::BinaryV0 + )); + } + + #[test] + fn resolve_trace_format_rejects_unknown_values() { + Python::with_gil(|py| { + let err = resolve_trace_format("yaml").expect_err("should reject yaml"); + assert_eq!(err.get_type(py).name().expect("type name"), "RuntimeError"); + let message = err.value(py).to_string(); + assert!(message.contains("unsupported trace format")); + }); + } + + #[test] + fn collect_program_metadata_reads_sys_argv() { + Python::with_gil(|py| { + let sys = py.import("sys").expect("import sys"); + let original = sys.getattr("argv").expect("argv").unbind(); + let argv = PyList::new(py, ["/tmp/prog.py", "--flag", "value"]).expect("argv"); + sys.setattr("argv", argv).expect("set argv"); + + let result = collect_program_metadata(py); + sys.setattr("argv", original.bind(py)) + .expect("restore argv"); + + let metadata = result.expect("metadata"); + assert_eq!(metadata.program, "/tmp/prog.py"); + assert_eq!( + metadata.args, + vec!["--flag".to_string(), "value".to_string()] + ); + }); + } + + #[test] + fn collect_program_metadata_defaults_unknown_program() { + Python::with_gil(|py| { + let sys = py.import("sys").expect("import sys"); + let original = sys.getattr("argv").expect("argv").unbind(); + let empty = PyList::empty(py); + sys.setattr("argv", empty).expect("set empty argv"); + + let result = collect_program_metadata(py); + sys.setattr("argv", original.bind(py)) + .expect("restore argv"); + + let metadata = result.expect("metadata"); + assert_eq!(metadata.program, ""); + assert!(metadata.args.is_empty()); + }); + } + + #[test] + fn prepare_bootstrap_populates_fields_and_creates_directory() { + Python::with_gil(|py| { + let tmp = tempdir().expect("tempdir"); + let trace_dir = tmp.path().join("out"); + let activation = tmp.path().join("entry.py"); + std::fs::write(&activation, "print('hi')\n").expect("write activation file"); + + let sys = py.import("sys").expect("import sys"); + let original = sys.getattr("argv").expect("argv").unbind(); + let program_str = activation.to_str().expect("utf8 path"); + let argv = PyList::new(py, [program_str, "--verbose"]).expect("argv"); + sys.setattr("argv", argv).expect("set argv"); + + let result = TraceSessionBootstrap::prepare( + py, + trace_dir.as_path(), + "json", + Some(activation.as_path()), + ); + sys.setattr("argv", original.bind(py)) + .expect("restore argv"); + + let bootstrap = result.expect("bootstrap"); + assert!(trace_dir.is_dir()); + assert_eq!(bootstrap.trace_directory(), trace_dir.as_path()); + assert!(matches!(bootstrap.format(), TraceEventsFileFormat::Json)); + assert_eq!(bootstrap.activation_path(), Some(activation.as_path())); + assert_eq!(bootstrap.program(), program_str); + let expected_args: Vec = vec!["--verbose".to_string()]; + assert_eq!(bootstrap.args(), expected_args.as_slice()); + }); + } +} diff --git a/codetracer-python-recorder/tests/README.md b/codetracer-python-recorder/tests/README.md new file mode 100644 index 0000000..47222ef --- /dev/null +++ b/codetracer-python-recorder/tests/README.md @@ -0,0 +1,42 @@ +# Test Layout + +This crate now keeps all integration-style tests under a single `tests/` root so +developers can see which harness they are touching at a glance. + +- `python/` — Pytest and unittest suites that exercise the public Python API and + high-level tracing flows. Invoke with `uv run --group dev --group test pytest + codetracer-python-recorder/tests/python`. +- `rust/` — Rust integration tests that embed CPython through PyO3. These are + collected via the `tests/rust.rs` aggregator and run with `uv run cargo nextest + run --manifest-path codetracer-python-recorder/Cargo.toml --no-default-features`. +- Shared fixtures and helpers will live under `tests/support/` as they are + introduced in later stages of the improvement plan. + +For unit tests that do not require the FFI boundary, prefer `#[cfg(test)]` +modules co-located with the Rust source, or Python module-level tests inside the +`codetracer_python_recorder` package. + +## Coverage Workflow + +When you need coverage artefacts, prefer the Just helpers so local runs match the +CI configuration: + +- `just coverage-rust` runs `cargo llvm-cov nextest` with the same flags as the + Rust test job and writes `lcov.info` under + `codetracer-python-recorder/target/coverage/rust/`. The helper wires in + `LLVM_COV`/`LLVM_PROFDATA` from the dev shell (provided by + `llvmPackages_latest.llvm`), so make sure you re-enter `nix develop` after + pulling updates. It also captures `summary.json` and prints a per-file table + so the console mirrors the pytest coverage output. (Run + `cargo llvm-cov nextest --html` manually if you need a browsable report.) +- `just coverage-python` executes pytest with `pytest-cov`, limiting collection to + `codetracer_python_recorder` and emitting both `coverage.xml` and `coverage.json` + in `codetracer-python-recorder/target/coverage/python/`. +- `just coverage` is a convenience wrapper that invokes both commands in sequence. + +CI runs the same helper and posts a coverage summary comment on pull requests so +reviewers can see the per-file breakdown without downloading artefacts. + +All commands create their output directories on first run, so no manual setup is +required beyond entering the Nix shell (`nix develop`) or syncing the UV virtual +environment (`just venv`). diff --git a/codetracer-python-recorder/tests/python/__init__.py b/codetracer-python-recorder/tests/python/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/codetracer-python-recorder/test/smoke.py b/codetracer-python-recorder/tests/python/smoke.py similarity index 100% rename from codetracer-python-recorder/test/smoke.py rename to codetracer-python-recorder/tests/python/smoke.py diff --git a/codetracer-python-recorder/tests/python/support/__init__.py b/codetracer-python-recorder/tests/python/support/__init__.py new file mode 100644 index 0000000..e1d999b --- /dev/null +++ b/codetracer-python-recorder/tests/python/support/__init__.py @@ -0,0 +1,14 @@ +"""Shared helpers for Python test suites.""" +from __future__ import annotations + +from pathlib import Path + +__all__ = ["ensure_trace_dir"] + + +def ensure_trace_dir(root: Path, name: str = "trace_out") -> Path: + """Return an existing trace directory under ``root``, creating it if needed.""" + target = root / name + target.mkdir(exist_ok=True) + return target + diff --git a/codetracer-python-recorder/test/test_codetracer_api.py b/codetracer-python-recorder/tests/python/test_codetracer_api.py similarity index 100% rename from codetracer-python-recorder/test/test_codetracer_api.py rename to codetracer-python-recorder/tests/python/test_codetracer_api.py diff --git a/codetracer-python-recorder/test/test_monitoring_events.py b/codetracer-python-recorder/tests/python/test_monitoring_events.py similarity index 98% rename from codetracer-python-recorder/test/test_monitoring_events.py rename to codetracer-python-recorder/tests/python/test_monitoring_events.py index bc9535f..aca890e 100644 --- a/codetracer-python-recorder/test/test_monitoring_events.py +++ b/codetracer-python-recorder/tests/python/test_monitoring_events.py @@ -9,6 +9,8 @@ import codetracer_python_recorder as codetracer +from .support import ensure_trace_dir + @dataclass class ParsedTrace: @@ -80,8 +82,7 @@ def _write_script(tmp: Path) -> Path: def test_py_start_line_and_return_events_are_recorded(tmp_path: Path) -> None: # Arrange: create a script and start tracing with activation restricted to that file script = _write_script(tmp_path) - out_dir = tmp_path / "trace_out" - out_dir.mkdir() + out_dir = ensure_trace_dir(tmp_path) session = codetracer.start(out_dir, format=codetracer.TRACE_JSON, start_on_enter=script) @@ -132,8 +133,7 @@ def test_py_start_line_and_return_events_are_recorded(tmp_path: Path) -> None: def test_start_while_active_raises(tmp_path: Path) -> None: - out_dir = tmp_path / "trace_out" - out_dir.mkdir() + out_dir = ensure_trace_dir(tmp_path) session = codetracer.start(out_dir, format=codetracer.TRACE_JSON) try: with pytest.raises(RuntimeError): diff --git a/codetracer-python-recorder/test/test_smoke.py b/codetracer-python-recorder/tests/python/test_smoke.py similarity index 100% rename from codetracer-python-recorder/test/test_smoke.py rename to codetracer-python-recorder/tests/python/test_smoke.py diff --git a/codetracer-python-recorder/tests/python/unit/test_session_helpers.py b/codetracer-python-recorder/tests/python/unit/test_session_helpers.py new file mode 100644 index 0000000..266a146 --- /dev/null +++ b/codetracer-python-recorder/tests/python/unit/test_session_helpers.py @@ -0,0 +1,122 @@ +"""Unit tests for session helper functions.""" +from __future__ import annotations + +from pathlib import Path + +import pytest + +from codetracer_python_recorder import session + + +def test_coerce_format_accepts_supported_aliases() -> None: + assert session._coerce_format("json") == "json" + assert session._coerce_format("JSON") == "json" + assert session._coerce_format("binary") == "binary" + + +def test_coerce_format_rejects_unknown_value() -> None: + with pytest.raises(ValueError) as excinfo: + session._coerce_format("yaml") + assert "unsupported trace format" in str(excinfo.value) + + +def test_validate_trace_path_expands_user(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + home_dir = tmp_path / "home" + home_dir.mkdir() + target = home_dir / "traces" + + monkeypatch.setenv("HOME", str(home_dir)) + path = session._validate_trace_path(Path("~/traces")) + + assert path == target + assert path.parent == home_dir + + +def test_validate_trace_path_rejects_file(tmp_path: Path) -> None: + file_path = tmp_path / "trace.bin" + file_path.write_text("stub") + with pytest.raises(ValueError): + session._validate_trace_path(file_path) + + +def test_normalize_activation_path_handles_none() -> None: + assert session._normalize_activation_path(None) is None + + +def test_normalize_activation_path_expands_user(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + home_dir = tmp_path / "home" + home_dir.mkdir() + script = home_dir / "script.py" + script.write_text("print('hi')\n") + + monkeypatch.setenv("HOME", str(home_dir)) + result = session._normalize_activation_path("~/script.py") + + assert result == str(script) + + +@pytest.fixture(autouse=True) +def clear_active_session() -> None: + session._active_session = None + yield + session._active_session = None + + +def test_trace_session_stop_clears_global(monkeypatch: pytest.MonkeyPatch) -> None: + called = {"stop": False, "start": False} + + def fake_start(*args, **kwargs) -> None: + called["start"] = True + + def fake_stop() -> None: + called["stop"] = True + + monkeypatch.setattr(session, "_start_backend", fake_start) + monkeypatch.setattr(session, "_stop_backend", fake_stop) + monkeypatch.setattr(session, "_is_tracing_backend", lambda: session._active_session is not None) + + session._active_session = session.TraceSession(path=Path("/tmp"), format="json") + session.stop() + assert session._active_session is None + assert called["stop"] is True + + +def test_trace_session_flush_noop_when_inactive(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(session, "_is_tracing_backend", lambda: False) + flushed = [] + + def fake_flush() -> None: + flushed.append(True) + + monkeypatch.setattr(session, "_flush_backend", fake_flush) + session.flush() + assert flushed == [] + + +def test_trace_context_manager_starts_and_stops(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: + calls = {"start": [], "stop": []} + + trace_state = {"active": False} + + def fake_start(path: str, fmt: str, activation: str | None) -> None: + trace_state["active"] = True + calls["start"].append((Path(path), fmt, activation)) + + def fake_stop() -> None: + trace_state["active"] = False + calls["stop"].append(True) + + monkeypatch.setattr(session, "_start_backend", fake_start) + monkeypatch.setattr(session, "_stop_backend", fake_stop) + monkeypatch.setattr(session, "_is_tracing_backend", lambda: trace_state["active"]) + monkeypatch.setattr(session, "_flush_backend", lambda: None) + + target = tmp_path / "trace" + target.mkdir() + with session.trace(target, format="binary") as handle: + assert isinstance(handle, session.TraceSession) + assert handle.path == target.expanduser() + assert handle.format == "binary" + + assert calls["start"] == [(target, "binary", None)] + assert calls["stop"] == [True] diff --git a/codetracer-python-recorder/tests/rust.rs b/codetracer-python-recorder/tests/rust.rs new file mode 100644 index 0000000..2d553a5 --- /dev/null +++ b/codetracer-python-recorder/tests/rust.rs @@ -0,0 +1,7 @@ +//! Aggregated Rust integration tests for codetracer-python-recorder. + +#[path = "rust/code_object_wrapper.rs"] +mod code_object_wrapper; + +#[path = "rust/print_tracer.rs"] +mod print_tracer; diff --git a/codetracer-python-recorder/tests/code_object_wrapper.rs b/codetracer-python-recorder/tests/rust/code_object_wrapper.rs similarity index 100% rename from codetracer-python-recorder/tests/code_object_wrapper.rs rename to codetracer-python-recorder/tests/rust/code_object_wrapper.rs diff --git a/codetracer-python-recorder/tests/print_tracer.rs b/codetracer-python-recorder/tests/rust/print_tracer.rs similarity index 100% rename from codetracer-python-recorder/tests/print_tracer.rs rename to codetracer-python-recorder/tests/rust/print_tracer.rs diff --git a/design-docs/adr/0003-test-suite-governance.md b/design-docs/adr/0003-test-suite-governance.md new file mode 100644 index 0000000..7fe0dc4 --- /dev/null +++ b/design-docs/adr/0003-test-suite-governance.md @@ -0,0 +1,76 @@ +# ADR 0003: Test Suite Governance for codetracer-python-recorder + +- **Status:** Accepted +- **Date:** 2025-10-02 +- **Deciders:** Platform / Runtime Tracing Team +- **Consulted:** Python Tooling WG, Developer Experience WG +- **Informed:** Reliability Engineering Guild + +## Context + +`codetracer-python-recorder` currently depends on three distinct harnesses: Rust unit tests inside the crate, Rust integration tests under `codetracer-python-recorder/tests/`, and Python tests under `codetracer-python-recorder/test/`. `just test` wires these together via `cargo nextest run` and `pytest`, but we do not document which behaviours belong to each layer. As a result: +- Contributors duplicate coverage (e.g., API happy paths exist both in Rust integration tests and Python tests) while other areas are untested (no references to `TraceSessionBootstrap::prepare`, `ensure_trace_directory`, or `TraceOutputPaths::configure_writer`). +- The `test/` vs `tests/` split is opaque to new maintainers and tooling; several CI linters only recurse into `tests/`, so Python-only changes can silently reduce coverage. +- Developers add integration-style assertions to Python tests that require spawning interpreters, even when the logic could be exercised cheaply in Rust. +- Doc examples risk drifting from executable reality because doctests are disabled to avoid invoking the CPython runtime. + +Without a clear taxonomy for Rust vs. Python coverage, the test surface is growing unevenly and critical bootstrap/activation code remains unverified. + +## Decision + +We will adopt a tiered test governance model and reorganise the repository to make the boundaries explicit. + +1. **Define a test taxonomy.** + - `src/**/*.rs` unit tests (behind `#[cfg(test)]`) cover pure-Rust helpers, pointer/FFI safety shims, and error handling that does not need to cross the FFI boundary. + - `codetracer-python-recorder/tests/rust/**` integration tests exercise PyO3 + CPython interactions (e.g., `CodeObjectRegistry`, `RuntimeTracer` callbacks) and may spin up embedded interpreters. + - `codetracer-python-recorder/tests/python/**` houses all Python-driven tests (pytest/unittest) for public APIs, end-to-end tracing flows, and environment bootstrapping. + - Documentation examples use doctests only when they can run without Python (otherwise they move into the appropriate test layer). + +2. **Restructure the repository.** + - Rename the existing Python `test/` directory to `tests/python/` and update tooling (`pytest` discovery, `pyproject.toml`, `Justfile`) accordingly. + - Move Rust integration tests into `tests/rust/` (keeping module names unchanged) to mirror the taxonomy. + - Introduce a `tests/README.md` that summarises the policy for future contributors. + +3. **Codify placement rules.** + - Every new test must state its target layer in the PR description and follow the directory conventions above. + - Changes touching PyO3 shims (`session`, `runtime`, `monitoring`) must include at least one Rust test; changes to the Python facade (`codetracer_python_recorder`) must include Python coverage unless the change is rust-only plumbing. + - Shared fixtures (temporary trace directories, sample scripts) live under `tests/support/` and are imported from both Rust and Python harnesses to avoid drift. + +4. **Fill immediate coverage gaps.** + - Add focused Rust unit tests for `TraceSessionBootstrap::prepare`, `ensure_trace_directory`, `resolve_trace_format`, and `collect_program_metadata`, including error paths (non-directory target, unsupported format, missing `sys.argv`). + - Add unit tests for `TraceOutputPaths::new` and `configure_writer` to ensure the writer initialises metadata/events files and starts at the expected location. + - Add deterministic tests for `ActivationController` covering activation on enter, deactivation on return, and behaviour when frames originate from synthetic filenames. + - Extend Python tests to cover `_normalize_activation_path` and failure modes of `_coerce_format`/`_validate_trace_path` without booting the Rust tracer. + +5. **Establish guardrails.** + - Update CI to run `cargo nextest run --workspace --tests` and `pytest tests/python` explicitly, making the split visible in logs. + - Track per-layer test counts in `tests/README.md` and flag regressions in coverage reports once we integrate coverage tooling. + +## Consequences + +- **Positive:** + - Onboarding improves because contributors follow a documented decision tree when adding tests. + - Critical bootstrap/activation paths gain deterministic unit coverage, reducing reliance on slow end-to-end scripts. + - CI output clarifies which harness failed, shortening the feedback loop. + - Shared fixtures reduce duplication between Rust and Python tests. + +- **Negative / Risks:** + - The directory rename requires touch-ups in existing scripts, IDE run configurations, and documentation. + - Contributors must learn the taxonomy, and reviews need to police placement for a few weeks. + - Running Python tests from a subdirectory may miss legacy tests until the migration completes; mitigated by performing the move in the same PR as the tooling updates. + +## Implementation Notes + +- Perform restructuring in stages (rename directories, update tooling, then move tests) to keep diffs reviewable. +- Introduce helper crates/modules under `tests/support/` to share temporary-directory setup between Rust and Python as soon as the taxonomy lands. +- Add `ruff` and `cargo fmt` hooks to ensure moved tests stay linted after the reorganisation. + +## Status Tracking + +- This ADR is **Accepted**. Directory restructuring, unit/integration coverage for the targeted modules, and the split CI/coverage jobs have landed; future adjustments will be tracked in follow-up ADRs if required. + +## Alternatives Considered + +- **Keep the current layout and document it informally.** Rejected because the `test/` vs `tests/` split is already causing confusion and does not solve the missing coverage gaps. +- **Create a monolithic Python integration test harness only.** Rejected because many FFI safety checks are cheaper to assert in Rust without spinning up subprocesses. +- **Adopt a coverage percentage gate.** Deferred until we have stable baselines; enforcing a percentage before addressing the structural issues would block unrelated work. diff --git a/design-docs/adr/0004-error-handling-policy.md b/design-docs/adr/0004-error-handling-policy.md new file mode 100644 index 0000000..bf1627a --- /dev/null +++ b/design-docs/adr/0004-error-handling-policy.md @@ -0,0 +1,105 @@ +# ADR 0004: Error Handling Policy for codetracer-python-recorder + +- **Status:** Proposed +- **Date:** 2025-10-02 +- **Deciders:** Runtime Tracing Maintainers +- **Consulted:** Python Tooling WG, Observability WG +- **Informed:** Developer Experience WG, Release Engineering + +## Context + +The Rust-backed recorder currently propagates errors piecemeal: +- PyO3 entry points bubble up plain `PyRuntimeError` instances with free-form strings (e.g., `src/session.rs:21-52`, `src/runtime/mod.rs:77-126`). +- Runtime helpers panic on invariant violations, which will abort the host interpreter because we do not fence panics at the FFI boundary (`src/runtime/mod.rs:107-120`, `src/runtime/activation.rs:24-33`, `src/runtime/value_encoder.rs:61-78`). +- Monitoring callbacks rely on `GLOBAL.lock().unwrap()` so poisoned mutexes or lock errors terminate the process (`src/monitoring/tracer.rs:268` and subsequent callback shims). +- Python helpers expose bare `RuntimeError`/`ValueError` without linking to a shared policy, and auto-start simply re-raises whatever the Rust layer emits (`codetracer_python_recorder/session.py:27-63`, `codetracer_python_recorder/auto_start.py:24-36`). +- Exit codes, log destinations, and trace-writer fallback behaviour are implicit; a disk-full failure today yields a generic exception and can leave partially written outputs. + +The lack of a central error façade makes it hard to enforce user-facing guarantees, reason about detaching vs aborting behaviour, or meet the operational goals we have been given: stable error codes, structured logs, optional JSON diagnostics, policy switches, and atomic trace outputs. + +## Decision + +We will introduce a recorder-wide error handling policy centred on a dedicated `recorder-errors` crate and a Python exception hierarchy. The policy follows fifteen guiding principles supplied by operations and is designed so the “right way” is the only easy way for contributors. + +### 1. Single Error Façade +- Create a new workspace crate `recorder-errors` exporting `RecorderError`, a structural error type with fields `{ kind: ErrorKind, code: ErrorCode, message: Cow<'static, str>, context: ContextMap, source: RecorderErrorSource }`. +- Provide `RecorderResult = Result` and convenience macros (`usage!`, `enverr!`, `target!`, `bug!`, `ensure_usage!`, `ensure_env!`, etc.) so Rust modules can author classified failures with one line. +- Require every other crate (including the PyO3 module) to depend on `recorder-errors`; direct construction of `PyErr`/`io::Error` is disallowed outside the façade. +- Maintain `ErrorCode` as a small, grep-able enum (e.g., `ERR_TRACE_DIR_NOT_DIR`, `ERR_FORMAT_UNSUPPORTED`), with documentation in the crate so codes stay stable across releases. + +### 2. Clear Classification & Exit Codes +- Define four top-level `ErrorKind` variants: + - `Usage` (caller mistakes, bad flags, conflicting sessions). + - `Environment` (IO, permissions, resource exhaustion). + - `Target` (user code raised or misbehaved while being traced). + - `Internal` (bugs, invariants, unexpected panics). +- Map kinds to fixed process exit codes (`Usage=2`, `Environment=10`, `Target=20`, `Internal=70`). These are surfaced by CLI utilities and exported via the Python module for embedding tooling. +- Document canonical examples for each kind in the ADR appendix and in crate docs. + +### 3. FFI Safety & Python Exceptions +- Add an `ffi` module that wraps every `#[pyfunction]` with `catch_unwind`, converts `RecorderError` into a custom Python exception hierarchy (`RecorderError` base, subclasses `UsageError`, `EnvironmentError`, `TargetError`, `InternalError`), and logs panic payloads before mapping them to `InternalError`. +- PyO3 callbacks (`install_tracer`, monitoring trampolines) will run through `ffi::dispatch`, ensuring we never leak panics across the boundary. + +### 4. Output Channels & Diagnostics +- Forbid `println!`/`eprintln!` outside the logging module; diagnostic output goes to stderr via `tracing`/`log` infrastructure. +- Introduce a structured logging wrapper that attaches `{ run_id, trace_id, error_code }` fields to every error record. Provide `--log-level`, `--log-file`, and `--json-errors` switches that route structured diagnostics either to stderr or a configured file. + +### 5. Policy Switches +- Introduce a runtime policy singleton (`RecorderPolicy` stored in `OnceCell`) configured via CLI flags or environment variables: `--on-recorder-error=abort|disable`, `--require-trace`, `--keep-partial-trace`. +- Define semantics: `abort` -> propagate error and non-zero exit; `disable` -> detach tracer, emit structured warning, continue host process. Document exit codes for each combination in module docs. + +### 6. Atomic, Truthful Outputs +- Wrap trace writes behind an IO façade that stages files in a temp directory and performs atomic rename on success. +- When `--keep-partial-trace` is enabled, mark outputs with a `partial=true`, `reason=` trailer. Otherwise ensure no trace files are left behind on failure. + +### 7. Assertions with Containment +- Replace `expect`/`unwrap` (e.g., `src/runtime/mod.rs:114`, `src/runtime/activation.rs:26`, `src/runtime/value_encoder.rs:70`) with classified `bug!` assertions that convert to `RecorderError` while still triggering `debug_assert!` in dev builds. +- Document invariants in the new crate and ensure fuzzing/tests observe the diagnostics. + +### 8. Preflight Checks +- Centralise version/compatibility checks in a `preflight` module called from `start_tracing`. Validate Python major.minor, ABI compatibility, trace schema version, and feature flags before installing monitoring callbacks. +- Embed recorder version, schema version, and policy hash into every trace metadata file via `TraceWriter` extensions. + +### 9. Observability & Metrics +- Emit structured counters for key error pathways (dropped events, detach reasons, panics caught). Provide a `RecorderMetrics` sink with a no-op default and an optional exporter trait. +- When `--json-errors` is set, append a single-line JSON trailer to stderr containing `{ "error_code": .., "kind": .., "message": .., "context": .. }`. + +### 10. Failure-Path Testing +- Add exhaustive unit tests in `recorder-errors` for every `ErrorCode` and conversion path. +- Extend Rust integration tests to simulate disk-full (`ENOSPC`), permission denied, target exceptions, callback panics, SIGINT during detach, and partial trace recovery. +- Add Python tests asserting the custom exception hierarchy and policy toggles behave as documented. + +### 11. Performance-Aware Defences +- Reserve heavyweight diagnostics (stack captures, large context maps) for error paths. Hot callbacks use cheap checks (`debug_assert!` in release builds). Provide sampled validation hooks if additional runtime checks become necessary. + +### 12. Tooling Enforcement +- Add workspace lints (`deny(panic_in_result_fn)`, Clippy config) and a `just lint-errors` task that fails if `panic!`, `unwrap`, or `expect` appear outside `recorder-errors`. +- Disallow `anyhow`/`eyre` except inside the error façade with documented justification. + +### 13. Developer Ergonomics +- Export prelude modules (`use recorder_errors::prelude::*;`) so contributors get macros and types with a single import. +- Provide cookbook examples in the crate documentation and link the ADR so developers know how to map new errors to codes quickly. + +### 14. Documented Guarantees +- Document, in README + crate docs, the three promises: no stdout writes, trace outputs are atomic (or explicitly partial), and error codes stay stable within a minor version line. + +### 15. Scope & Non-Goals +- The recorder never aborts the host process; even internal bugs downgrade to `InternalError` surfaced through policy switches. +- Business-specific retention, shipping logs, or analytics integrations remain out of scope for this ADR. + +## Consequences + +- **Positive:** Structured errors enable user tooling, stable exit codes improve scripting, and panics are contained so we remain embedder-friendly. Central macros reduce boilerplate and make reviewers enforce policy easily. +- **Negative / Risks:** Introducing a new crate and policy layer adds upfront work and requires retrofitting existing call sites. Atomic IO staging may increase disk usage for large traces. Contributors must learn the new taxonomy and update tests accordingly. + +## Rollout & Status Tracking + +- Implementation proceeds under a dedicated plan (see "Error Handling Implementation Plan"). The ADR moves to **Accepted** once the façade crate, FFI wrappers, and policy switches are merged, and the legacy ad-hoc errors are removed. +- Future adjustments (e.g., new error codes) must update `recorder-errors` documentation and ensure backward compatibility for exit codes. + +## Alternatives Considered + +- **Use `anyhow` throughout and convert at the boundary.** Rejected because it obscures error provenance, offers no stable codes, and encourages stringly-typed errors. +- **Catch panics lazily within individual callbacks.** Rejected; a central wrapper keeps the policy uniform and ensures we do not miss newer entry points. +- **Rely on existing logging without policy switches.** Rejected because operational requirements demand scriptable behaviour on failure. + diff --git a/design-docs/error-handling-implementation-plan.md b/design-docs/error-handling-implementation-plan.md new file mode 100644 index 0000000..0abff33 --- /dev/null +++ b/design-docs/error-handling-implementation-plan.md @@ -0,0 +1,92 @@ +# codetracer-python-recorder Error Handling Implementation Plan + +## Goals +- Deliver the policy defined in ADR 0004: every error flows through `RecorderError`, surfaces a stable code/kind, and maps to the Python exception hierarchy. +- Contain all panics within the FFI boundary and offer deterministic behaviour for `abort` versus `disable` policies. +- Ensure trace outputs remain atomic (or explicitly marked partial) and diagnostics never leak to stdout. +- Provide developers with ergonomic macros, tooling guardrails, and comprehensive tests covering failure paths. + +## Current Gaps +- Ad-hoc `PyRuntimeError` strings in `src/session.rs:21-76` and `src/runtime/mod.rs:77-190` prevent stable categorisation and user scripting. +- FFI trampolines in `src/monitoring/tracer.rs:268-706` and activation helpers in `src/runtime/activation.rs:24-83` still use `unwrap`/`expect`, so poisoned locks or filesystem errors abort the interpreter. +- Python facade functions (`codetracer_python_recorder/session.py:27-63`) return built-in exceptions and provide no context or exit codes. +- No support for JSON diagnostics, policy switches, or atomic output staging; disk failures can leave half-written traces and logs mix stdout/stderr. + +## Workstreams + +### WS1 – Foundations & Inventory +- Add a `just errors-audit` command that runs `rg` to list `PyRuntimeError`, `unwrap`, `expect`, and direct `panic!` usage in the recorder crate. +- Create issue tracker entries grouping call sites by module (`session`, `runtime`, `monitoring`, Python facade) to guide refactors. +- Exit criteria: checklist of legacy error sites recorded with owners. + +### WS2 – `recorder-errors` Crate +- Scaffold `recorder-errors` under the workspace with `RecorderError`, `RecorderResult`, `ErrorKind`, `ErrorCode`, context map type, and conversion traits from `io::Error`, `PyErr`, etc. +- Implement ergonomic macros (`usage!`, `enverr!`, `target!`, `bug!`, `ensure_*`) plus unit tests covering formatting, context propagation, and downcasting. +- Publish crate docs explaining mapping rules and promises; link ADR 0004. +- Exit criteria: `cargo test -p recorder-errors` covers all codes; workspace builds with the new crate. + +### WS3 – Retrofit Rust Modules +- Replace direct `PyRuntimeError` construction in `src/session/bootstrap.rs`, `src/session.rs`, `src/runtime/mod.rs`, `src/runtime/output_paths.rs`, and helpers with `RecorderResult` + macros. +- Update `RuntimeTracer` to propagate structured errors instead of strings; remove `expect`/`unwrap` in hot paths by returning classified `bug!` or `enverr!` failures. +- Introduce a small adapter in `src/runtime/mod.rs` that stages IO writes and applies the atomic/partial policy described in ADR 0004. +- Exit criteria: All recorder crate modules compile without `pyo3::exceptions::PyRuntimeError::new_err` usage. + +### WS4 – FFI Wrapper & Python Exception Hierarchy +- Implement `ffi::wrap_pyfunction` that catches panics (`std::panic::catch_unwind`), maps `RecorderError` to a new `PyRecorderError` base type plus subclasses (`PyUsageError`, `PyEnvironmentError`, etc.). +- Update `#[pymodule]` and every `#[pyfunction]` to use the wrapper; ensure monitoring callbacks also go through the dispatcher. +- Expose the exception types in `codetracer_python_recorder/__init__.py` for Python callers. +- Exit criteria: Rust panics surface as `PyInternalError`, and Python tests can assert exception class + code. + +### WS5 – Policy Switches & Runtime Configuration +- Add `RecorderPolicy` backed by `OnceCell` with setters for CLI flags/env vars: `--on-recorder-error`, `--require-trace`, `--keep-partial-trace`, `--log-level`, `--log-file`, `--json-errors`. +- Update the CLI/embedding entry points (auto-start, `TraceSession`) to fill the policy before starting tracing. +- Implement detach vs abort semantics in `RuntimeTracer::finish` / session stop paths, honoring policy decisions and exit codes. +- Exit criteria: Integration tests demonstrate both `abort` and `disable` flows, including partial trace handling. + +### WS6 – Logging, Metrics, and Diagnostics +- Replace `env_logger` initialisation with a `tracing` subscriber or structured `log` formatter that includes `run_id`, `trace_id`, and `ErrorCode` fields. +- Emit counters for dropped events, detach reasons, and caught panics via a `RecorderMetrics` sink (default no-op, pluggable in future). +- Implement `--json-errors` to emit a single-line JSON trailer on stderr whenever an error is returned to Python. +- Exit criteria: Structured log output verified in tests; stdout usage gated by lint. + +### WS7 – Test Coverage & Tooling Enforcement +- Add unit tests for the new error crate, IO façade, policy switches, and FFI wrappers (panic capture, exception mapping). +- Extend Python tests to cover the new exception hierarchy, JSON diagnostics, and policy flags. +- Introduce CI lints (`cargo clippy --deny clippy::panic`, custom script rejecting `unwrap` outside allowed modules) and integrate with `just lint`. +- Exit criteria: CI blocks regressions; failure-path tests cover disk full, permission denied, target exceptions, partial trace recovery, and SIGINT during detach. + +### WS8 – Documentation & Rollout +- Update README, API docs, and onboarding material to describe guarantees, exit codes, example snippets, and migration guidance for downstream tools. +- Add a change log entry summarising the policy and how to consume structured errors from Python. +- Track adoption status in `design-docs/error-handling-implementation-plan.status.md` (mirror existing planning artifacts). +- Exit criteria: Documentation merged, status file created, ADR 0004 promoted to **Accepted** once WS2–WS7 land. + +## Milestones & Sequencing +1. **Milestone A – Foundations:** Complete WS1 and WS2 (error crate scaffold) in parallel; unblock later work. +2. **Milestone B – Core Refactor:** Deliver WS3 and WS4 together so Rust modules emit structured errors and Python sees the new exceptions. +3. **Milestone C – Policy & IO Guarantees:** Finish WS5 and WS6 to stabilise runtime behaviour and diagnostics. +4. **Milestone D – Hardening:** Execute WS7 (tests, tooling) and WS8 (documentation). Promote ADR 0004 to Accepted. + +## Verification Strategy +- Add a `just test-errors` recipe running targeted failure tests (disk-full, detach, panic capture) plus Python unit tests for error classes. +- Use `cargo nextest run -p codetracer-python-recorder --features failure-fixtures` to execute synthetic failure cases. +- Enable `pytest tests/python/error_handling -q` for Python-specific coverage. +- Capture structured stderr in integration tests to assert JSON trailers and exit codes. + +## Dependencies & Coordination +- Requires consensus with the Observability WG on log format fields and exit-code mapping. +- Policy flag wiring depends on any CLI/front-end work planned for Q4; coordinate with developer experience owners. +- If `runtime_tracing` needs extensions for metadata trailers, align timelines with that team. + +## Risks & Mitigations +- **Wide-scope refactor:** Stage work behind feature branches and land per-module PRs to avoid blocking releases. +- **Performance regressions:** Benchmark hot callbacks before/after WS3 using existing microbenchmarks; keep additional allocations off hot paths. +- **API churn for users:** Provide compatibility shims that map old exceptions to new ones for at least one minor release, and document upgrade notes. +- **Partial trace semantics confusion:** Default to `abort` (no partial outputs) unless `--keep-partial-trace` is explicit; emit warnings when users opt in. + +## Done Definition +- Legacy `PyRuntimeError::new_err` usage is removed or isolated to compat shims. +- All panics are caught before crossing into Python; fuzz tests confirm no UB. +- `just test` (and targeted error suites) pass on Linux/macOS CI, with new structured logs and metrics visible. +- Documentation reflects guarantees, and downstream teams acknowledge new exit codes. + diff --git a/design-docs/test-suite-coverage-plan.md b/design-docs/test-suite-coverage-plan.md new file mode 100644 index 0000000..7f8e4d4 --- /dev/null +++ b/design-docs/test-suite-coverage-plan.md @@ -0,0 +1,65 @@ +# Test Suite Coverage Plan for codetracer-python-recorder + +## Goals +- Provide lightweight code coverage signals for both the Rust and Python layers without blocking CI on initial roll-out. +- Enable engineers to inspect coverage reports for targeted modules (runtime activation, session bootstrap, Python facade helpers) while keeping runtimes acceptable. +- Lay groundwork for future gating (e.g., minimum coverage thresholds) once the numbers stabilise. + +## Tooling Choices +- **Rust:** Use `cargo llvm-cov` to aggregate unit and integration test coverage. This tool integrates with `nextest` and produces both lcov and HTML outputs. It works with the existing `nix develop` environment once `llvm-tools-preview` is available (already pulled by rustup in Nix environment). +- **Python:** Use `pytest --cov` with the `coverage` plugin. Restrict collection to the `codetracer_python_recorder` package to avoid noise from site-packages. Generate both terminal summaries and Cobertura XML for upload. + +## Prerequisites & Dependencies +- Add `cargo-llvm-cov` to the dev environment so the Just targets and CI runners share the same binary. In the Nix shell, include the package and ensure the Rust toolchain exposes `llvm-tools-preview` or equivalent `llvm` binaries. The current dev shell ships `llvmPackages_latest.llvm`, making `llvm-cov`/`llvm-profdata` available without rustup components. +- Extend the UV `dev` dependency group with `pytest-cov` and `coverage[toml]` so Python coverage instrumentation is reproducible locally and in CI. +- Standardise coverage outputs under `codetracer-python-recorder/target/coverage` to keep artefacts inside the Rust crate. Use `target/coverage/{rust,python}` for per-language assets and a top-level `index.txt` to note the run metadata if needed later. + +## Execution Strategy +1. **Local Workflow** + - Add convenience Just targets that mirror the default test steps: + - `just coverage-rust` → `LLVM_COV=$(command -v llvm-cov) LLVM_PROFDATA=$(command -v llvm-profdata) uv run cargo llvm-cov --manifest-path codetracer-python-recorder/Cargo.toml --no-default-features --nextest --lcov --output-path codetracer-python-recorder/target/coverage/rust/lcov.info`, followed by `cargo llvm-cov report --summary-only --json` to generate `summary.json` and a Python helper that prints a table mirroring the pytest coverage output. Document that contributors can run a second `cargo llvm-cov … --html --output-dir …` invocation when they need browsable reports because the CLI disallows combining `--lcov` and `--html` in a single run. + - `just coverage-python` → `uv run --group dev --group test pytest --cov=codetracer_python_recorder --cov-report=term --cov-report=xml:codetracer-python-recorder/target/coverage/python/coverage.xml --cov-report=json:codetracer-python-recorder/target/coverage/python/coverage.json codetracer-python-recorder/tests/python`. + - `just coverage` wrapper → runs the Rust step followed by the Python step so developers get both artefacts with one command, matching the eventual CI flow. + - Ensure the commands create their output directories (`target/coverage/rust` and `target/coverage/python`) before writing results to avoid failures on first use. + - Document the workflow in `codetracer-python-recorder/tests/README.md` (and reference the top-level `README` if needed) so contributors know when to run the coverage helpers versus the regular test splits. + +2. **CI Integration (non-blocking first pass)** + - Extend `.github/workflows/ci.yml` with optional `coverage-rust` and `coverage-python` jobs that depend on the primary test jobs and only run when `matrix.python-version == '3.12'` and `matrix.os == 'ubuntu-latest'` to avoid duplicate collection. + - Reuse the Just targets so CI mirrors local behaviour. Inject `RUSTFLAGS`/`RUSTDOCFLAGS` from the test jobs’ cache to avoid rebuilding dependencies. + - Publish artefacts via `actions/upload-artifact`: + - Rust: `codetracer-python-recorder/target/coverage/rust/lcov.info`, the machine-readable `summary.json`, and optionally a gzipped HTML folder produced via a follow-up `cargo llvm-cov nextest --html --output-dir …` run in the same job. + - Python: `codetracer-python-recorder/target/coverage/python/coverage.xml` and `coverage.json` for downstream tooling. + - Mark coverage steps with `continue-on-error: true` during the stabilisation phase and note the run IDs in the job summary for quick retrieval. + - Use a GitHub Action to post/update a PR comment that embeds the Rust and Python coverage summaries in Markdown (via `scripts/generate_coverage_comment.py` drawing from the JSON reports), giving reviewers quick insight without opening artefacts. + +3. **Reporting & Visualisation** + - Use GitHub Actions artefacts for report retrieval. + - Investigate integration with Codecov or Coveralls once the raw reports stabilise; defer external upload until initial noise is assessed. + +## Incremental Roll-Out +1. Land Just targets and documentation so engineers can generate coverage locally. +2. Add CI coverage steps guarded by `if: matrix.python-version == '3.12'` to avoid duplicate work across versions. +3. Monitor runtimes and artefact sizes for a few cycles. +4. Once stable: + - Remove `continue-on-error` and make coverage generation mandatory. + - Introduce thresholds (e.g., fail if Rust line coverage < 70% or Python < 60%)—subject to discussion with the Runtime Tracing Team. + +## Implementation Checklist +- [x] Update development environment dependencies (`flake.nix`, `pyproject.toml`) to support coverage tooling out of the box. +- [x] Add `just coverage-rust`, `just coverage-python`, and `just coverage` helpers with directory bootstrapping. +- [x] Refresh documentation (`codetracer-python-recorder/tests/README.md` and top-level testing guide) with coverage instructions. +- [x] Extend CI workflow with non-blocking coverage jobs and artefact upload. +- [x] Review initial coverage artefacts to set baseline thresholds before enforcement. + +## Risks & Mitigations +- **Runtime overhead:** Coverage runs are slower. Mitigate by limiting to a single matrix entry and caching `target/coverage` directories if needed. +- **Report size:** HTML artefacts can be large. Compress before upload and prune historical runs as necessary. +- **PyO3 instrumentation quirks:** Ensure `cargo llvm-cov` runs with `--no-default-features` similar to existing `nextest` invocation to avoid mismatched Python symbols. +- **Coverage accuracy:** Python subprocess-heavy tests may under-report coverage. Supplement with targeted unit tests already added in Stage 4. + +## Next Actions +- Implement the local Just targets and update documentation. +- Extend CI workflow with optional coverage steps (post-tests) and artefact upload. +- Align with the developer experience team before enforcing thresholds. + +_Status tracking lives in `design-docs/test-suite-coverage-plan.status.md`._ diff --git a/design-docs/test-suite-coverage-plan.status.md b/design-docs/test-suite-coverage-plan.status.md new file mode 100644 index 0000000..809df37 --- /dev/null +++ b/design-docs/test-suite-coverage-plan.status.md @@ -0,0 +1,11 @@ +# Test Suite Coverage Plan Status + +## Current Status +- ✅ Plan doc expanded with prerequisites, detailed Just targets, CI strategy, and an implementation checklist (see `design-docs/test-suite-coverage-plan.md`). +- ✅ Implementation: coverage dependencies added to the dev shell (`flake.nix`) and UV groups (`pyproject.toml`). +- ✅ Implementation: `just coverage-*` helpers landed with matching documentation in `codetracer-python-recorder/tests/README.md`. +- ✅ Implementation: CI now runs `just coverage` on Python 3.12 with non-blocking jobs, uploads JSON/XML/LCOV artefacts, and posts a PR comment summarising Rust/Python coverage (`.github/workflows/ci.yml`). +- ✅ Assessment: capture baseline coverage numbers before proposing enforcement thresholds. + +## Next Steps +We are Done diff --git a/design-docs/test-suite-improvement-plan.md b/design-docs/test-suite-improvement-plan.md new file mode 100644 index 0000000..205a3ae --- /dev/null +++ b/design-docs/test-suite-improvement-plan.md @@ -0,0 +1,93 @@ +# codetracer-python-recorder Test Suite Improvement Plan + +## Goals +- Establish a transparent testing pyramid so engineers know whether new coverage belongs in Rust unit tests, Rust integration tests, or Python user-flow tests. +- Raise confidence in onboarding-critical paths (session bootstrap, activation gating, file outputs) by adding deterministic unit and integration tests. +- Reduce duplication and drift between Rust and Python harnesses by sharing fixtures and tooling. +- Prepare for future coverage metrics by making each harness runnable and observable in isolation. + +## Current Pain Points +- Python-facing tests currently live in `codetracer-python-recorder/test/` while Rust integration tests live in `codetracer-python-recorder/tests/`; the near-identical names are easy to mis-type and confuse CI/job configuration. +- Core bootstrap logic lacks direct coverage: no existing test references `TraceSessionBootstrap::prepare` or helpers inside `codetracer-python-recorder/src/session/bootstrap.rs`, and `TraceOutputPaths::configure_writer` in `codetracer-python-recorder/src/runtime/output_paths.rs` is only exercised implicitly. +- `ActivationController` in `codetracer-python-recorder/src/runtime/activation.rs` is only touched indirectly through long integration scripts, leaving edge cases (synthetic filenames, multiple activation toggles) unverified. +- Python helpers `_coerce_format`, `_validate_trace_path`, and `_normalize_activation_path` in `codetracer-python-recorder/codetracer_python_recorder/session.py` are not unit tested; regressions would surface only during end-to-end runs. +- `just test` hides which harness failed because both `cargo nextest run` and `pytest` report together; failures require manual reproduction to determine the responsible layer. + +## Workstreams + +### WS1 – Layout Consolidation & Tooling Updates +- Rename Python test directory to `codetracer-python-recorder/tests/python/` and move existing files. +- Move Rust integration tests into `codetracer-python-recorder/tests/rust/` and update `Cargo.toml` (if necessary) to ensure cargo discovers them. +- Add `codetracer-python-recorder/tests/README.md` describing the taxonomy and quick-start commands. +- Update `Justfile`, `pyproject.toml`, and any workflow scripts to call `pytest tests/python` explicitly. +- Exit criteria: `just test` logs identify `cargo nextest` and `pytest tests/python` as separate steps, and developers can run each harness independently. + +### WS2 – Rust Bootstrap Coverage +- Add `#[cfg(test)]` modules under `codetracer-python-recorder/src/session/bootstrap.rs` covering: + - Directory creation success and failure (non-directory path, unwritable path). + - Format resolution, including legacy aliases and error cases. + - Program metadata capture when `sys.argv` is empty or contains non-string values. +- Add tests for `TraceOutputPaths::new` and `configure_writer` under `codetracer-python-recorder/src/runtime/output_paths.rs`, using an in-memory writer stub to assert emitted file names and initial start position. +- Exit criteria: failures in any helper produce precise `PyRuntimeError` messages, and the new tests fail if error handling regresses. + +### WS3 – Activation Controller Guard Rails +- Introduce focused unit tests for `ActivationController` (e.g., `#[cfg(test)]` alongside `codetracer-python-recorder/src/runtime/activation.rs`) covering: + - Activation path matching and non-matching filenames. + - Synthetic filename rejection (`` and ``). + - Multiple activation cycles to ensure `activation_done` prevents re-entry. +- Extend existing `RuntimeTracer` tests to add a regression asserting that disabling synthetic frames keeps `CallbackOutcome::DisableLocation` consistent. +- Exit criteria: Activation tests run without spinning up full integration scripts and cover both positive and negative flows. + +### WS4 – Python API Unit Coverage & Fixtures +- Create a `tests/python/unit/` package with tests for `_coerce_format`, `_validate_trace_path`, `_normalize_activation_path`, and `TraceSession` life-cycle helpers (`flush`, `stop` when inactive). +- Extract reusable Python fixtures (temporary trace directory, environment manipulation) into `tests/python/support/` for reuse by integration tests. +- Confirm high-level tests (e.g., `test_monitoring_events.py`) import shared fixtures instead of duplicating temporary directory logic. +- Exit criteria: Python unit tests run without initialising the Rust extension, and integration tests rely on shared fixtures to minimise duplication. + +### WS5 – CI & Observability Enhancements +- Update CI workflows to surface separate status checks (e.g., `rust-tests`, `python-tests`). +- Add minimal coverage instrumentation: enable `cargo llvm-cov` (or `grcov`) for Rust helpers and `pytest --cov` for Python tests, even if we only publish the reports as artefacts initially. +- Document required commands in `tests/README.md` and ensure `just test` forwards `--nocapture`/`-q` flags appropriately. +- Exit criteria: CI reports the two harnesses independently, and developers can opt-in to coverage locally following documented steps. + +## Sequencing & Milestones + +1. **Stage 0 – Baseline (1 PR)** + - Capture current `just test` runtime and identify flaky tests. + - Snapshot trace files produced by `tests/test_monitoring_events.py` for regression comparison. + +2. **Stage 1 – Layout Consolidation (1–2 PRs)** + - Execute WS1: rename directories, update tooling, land `tests/README.md`. + +3. **Stage 2 – Bootstrap & Output Coverage (1 PR)** + - Execute WS2; ensure new tests pass on Linux/macOS runners. + +4. **Stage 3 – Activation Guard Rails (1 PR)** + - Execute WS3; ensure synthetic filename handling remains guarded. + +5. **Stage 4 – Python Unit Coverage (1 PR)** + - Execute WS4; migrate existing integration tests to shared fixtures. + +6. **Stage 5 – CI & Coverage Instrumentation (1 PR)** + - Execute WS5; update workflow files and document developer commands. + +7. **Stage 6 – Cleanup & Documentation (optional PR)** + - Update ADR status to **Accepted**, refresh onboarding docs, and archive baseline trace fixtures. + +## Verification Strategy +- Run `just test` after each stage; ensure both harnesses are explicitly reported in CI logs. +- Add `cargo nextest run --tests --nocapture activation::tests` smoke job to confirm activation unit coverage. +- For Python, run `pytest tests/python/unit -q` in isolation to keep the unit layer fast and deterministic. +- Compare stored trace fixtures before/after coverage additions to confirm no behavioural regressions. + +## Risks & Mitigations +- **Path renames break imports:** mitigate by landing directory changes alongside import updates and running `pytest -q` locally before merge. +- **Increased test runtime:** unit tests are lightweight; integration tests already dominate runtime. Monitor `just test` duration and consider parallel pytest execution if needed. +- **Coverage tooling churn:** start with optional coverage reports to avoid blocking CI; formal thresholds can follow once noise is understood. +- **PyO3 version mismatches:** ensure new Rust tests use `Python::with_gil` and `Bound<'_, PyAny>` consistently to avoid UB when running under coverage tools. + +## Deliverables & Ownership +- Primary owner: Runtime Tracing Team. +- Supporting reviewers: Python Tooling WG for Python fixtures and QA Automation Guild for CI changes. +- Target completion: end of Q4 FY25, ahead of planned streaming-writer work that depends on reliable regression coverage. + diff --git a/design-docs/test-suite-improvement-plan.status.md b/design-docs/test-suite-improvement-plan.status.md new file mode 100644 index 0000000..6b3a756 --- /dev/null +++ b/design-docs/test-suite-improvement-plan.status.md @@ -0,0 +1,26 @@ +# Test Suite Improvement Plan Status + +## Stage Summary +- ✅ Stage 0 – Baseline captured by ADR 0003 and the initial improvement plan. +- ✅ Stage 1 – Layout Consolidation: directory moves completed, test commands + updated, README added, and `just test` now runs the Rust and Python harnesses + separately. +- ✅ Stage 2 – Bootstrap & Output Coverage: unit tests now exercise + `TraceSessionBootstrap` helpers (directory/format/argv handling) and + `TraceOutputPaths::configure_writer`, with `just test` covering the new cases. +- ✅ Stage 3 – Activation Guard Rails: added unit tests around + `ActivationController` covering activation start, non-matching frames, and + deactivation behaviour; existing runtime integration tests continue to pass. +- ✅ Stage 4 – Python Unit Coverage: added `tests/python/unit/test_session_helpers.py` + for facade utilities and introduced `tests/python/support` for shared + fixtures; updated monitoring tests to use the helper directory builder. +- ✅ Stage 5 – CI & Coverage Instrumentation: CI now runs the split Rust/Python + test jobs plus a non-blocking coverage job that reuses `just coverage`, uploads + LCOV/XML/JSON artefacts, and posts a per-PR summary comment. +- ✅ Stage 6 – Cleanup & Documentation: ADR 0003 is now Accepted, top-level + docs describe the testing/coverage workflow, and the tests README references + the CI coverage comment for contributors. + +## Next Actions +Plan complete; monitor coverage baselines and propose enforcement thresholds in +a follow-up task. diff --git a/flake.nix b/flake.nix index e818bb8..f1db118 100644 --- a/flake.nix +++ b/flake.nix @@ -33,6 +33,8 @@ clippy rust-analyzer cargo-nextest + cargo-llvm-cov + llvmPackages_latest.llvm # Build tooling for Python extensions maturin diff --git a/pyproject.toml b/pyproject.toml index 9798331..d9c4da4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,6 +26,8 @@ typeCheckingMode = "basic" [dependency-groups] dev = [ "pytest>=8.3.5", + "pytest-cov>=5.0.0", + "coverage[toml]>=7.6.0", ] test = [ diff --git a/uv.lock b/uv.lock index dfbc830..a15db4b 100644 --- a/uv.lock +++ b/uv.lock @@ -30,8 +30,12 @@ source = { virtual = "." } [package.dev-dependencies] dev = [ + { name = "coverage", version = "7.6.1", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version < '3.9'" }, + { name = "coverage", version = "7.10.7", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version >= '3.9'" }, { name = "pytest", version = "8.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, { name = "pytest", version = "8.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "pytest-cov", version = "5.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "pytest-cov", version = "7.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, ] test = [ { name = "codetracer-pure-python-recorder" }, @@ -41,7 +45,11 @@ test = [ [package.metadata] [package.metadata.requires-dev] -dev = [{ name = "pytest", specifier = ">=8.3.5" }] +dev = [ + { name = "coverage", extras = ["toml"], specifier = ">=7.6.0" }, + { name = "pytest", specifier = ">=8.3.5" }, + { name = "pytest-cov", specifier = ">=5.0.0" }, +] test = [ { name = "codetracer-pure-python-recorder", editable = "codetracer-pure-python-recorder" }, { name = "codetracer-python-recorder", editable = "codetracer-python-recorder" }, @@ -56,6 +64,212 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] +[[package]] +name = "coverage" +version = "7.6.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f7/08/7e37f82e4d1aead42a7443ff06a1e406aabf7302c4f00a546e4b320b994c/coverage-7.6.1.tar.gz", hash = "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d", size = 798791, upload-time = "2024-08-04T19:45:30.9Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/61/eb7ce5ed62bacf21beca4937a90fe32545c91a3c8a42a30c6616d48fc70d/coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16", size = 206690, upload-time = "2024-08-04T19:43:07.695Z" }, + { url = "https://files.pythonhosted.org/packages/7d/73/041928e434442bd3afde5584bdc3f932fb4562b1597629f537387cec6f3d/coverage-7.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36", size = 207127, upload-time = "2024-08-04T19:43:10.15Z" }, + { url = "https://files.pythonhosted.org/packages/c7/c8/6ca52b5147828e45ad0242388477fdb90df2c6cbb9a441701a12b3c71bc8/coverage-7.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02", size = 235654, upload-time = "2024-08-04T19:43:12.405Z" }, + { url = "https://files.pythonhosted.org/packages/d5/da/9ac2b62557f4340270942011d6efeab9833648380109e897d48ab7c1035d/coverage-7.6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc", size = 233598, upload-time = "2024-08-04T19:43:14.078Z" }, + { url = "https://files.pythonhosted.org/packages/53/23/9e2c114d0178abc42b6d8d5281f651a8e6519abfa0ef460a00a91f80879d/coverage-7.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23", size = 234732, upload-time = "2024-08-04T19:43:16.632Z" }, + { url = "https://files.pythonhosted.org/packages/0f/7e/a0230756fb133343a52716e8b855045f13342b70e48e8ad41d8a0d60ab98/coverage-7.6.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34", size = 233816, upload-time = "2024-08-04T19:43:19.049Z" }, + { url = "https://files.pythonhosted.org/packages/28/7c/3753c8b40d232b1e5eeaed798c875537cf3cb183fb5041017c1fdb7ec14e/coverage-7.6.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c", size = 232325, upload-time = "2024-08-04T19:43:21.246Z" }, + { url = "https://files.pythonhosted.org/packages/57/e3/818a2b2af5b7573b4b82cf3e9f137ab158c90ea750a8f053716a32f20f06/coverage-7.6.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959", size = 233418, upload-time = "2024-08-04T19:43:22.945Z" }, + { url = "https://files.pythonhosted.org/packages/c8/fb/4532b0b0cefb3f06d201648715e03b0feb822907edab3935112b61b885e2/coverage-7.6.1-cp310-cp310-win32.whl", hash = "sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232", size = 209343, upload-time = "2024-08-04T19:43:25.121Z" }, + { url = "https://files.pythonhosted.org/packages/5a/25/af337cc7421eca1c187cc9c315f0a755d48e755d2853715bfe8c418a45fa/coverage-7.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0", size = 210136, upload-time = "2024-08-04T19:43:26.851Z" }, + { url = "https://files.pythonhosted.org/packages/ad/5f/67af7d60d7e8ce61a4e2ddcd1bd5fb787180c8d0ae0fbd073f903b3dd95d/coverage-7.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93", size = 206796, upload-time = "2024-08-04T19:43:29.115Z" }, + { url = "https://files.pythonhosted.org/packages/e1/0e/e52332389e057daa2e03be1fbfef25bb4d626b37d12ed42ae6281d0a274c/coverage-7.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3", size = 207244, upload-time = "2024-08-04T19:43:31.285Z" }, + { url = "https://files.pythonhosted.org/packages/aa/cd/766b45fb6e090f20f8927d9c7cb34237d41c73a939358bc881883fd3a40d/coverage-7.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff", size = 239279, upload-time = "2024-08-04T19:43:33.581Z" }, + { url = "https://files.pythonhosted.org/packages/70/6c/a9ccd6fe50ddaf13442a1e2dd519ca805cbe0f1fcd377fba6d8339b98ccb/coverage-7.6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d", size = 236859, upload-time = "2024-08-04T19:43:35.301Z" }, + { url = "https://files.pythonhosted.org/packages/14/6f/8351b465febb4dbc1ca9929505202db909c5a635c6fdf33e089bbc3d7d85/coverage-7.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6", size = 238549, upload-time = "2024-08-04T19:43:37.578Z" }, + { url = "https://files.pythonhosted.org/packages/68/3c/289b81fa18ad72138e6d78c4c11a82b5378a312c0e467e2f6b495c260907/coverage-7.6.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56", size = 237477, upload-time = "2024-08-04T19:43:39.92Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1c/aa1efa6459d822bd72c4abc0b9418cf268de3f60eeccd65dc4988553bd8d/coverage-7.6.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234", size = 236134, upload-time = "2024-08-04T19:43:41.453Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c8/521c698f2d2796565fe9c789c2ee1ccdae610b3aa20b9b2ef980cc253640/coverage-7.6.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133", size = 236910, upload-time = "2024-08-04T19:43:43.037Z" }, + { url = "https://files.pythonhosted.org/packages/7d/30/033e663399ff17dca90d793ee8a2ea2890e7fdf085da58d82468b4220bf7/coverage-7.6.1-cp311-cp311-win32.whl", hash = "sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c", size = 209348, upload-time = "2024-08-04T19:43:44.787Z" }, + { url = "https://files.pythonhosted.org/packages/20/05/0d1ccbb52727ccdadaa3ff37e4d2dc1cd4d47f0c3df9eb58d9ec8508ca88/coverage-7.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6", size = 210230, upload-time = "2024-08-04T19:43:46.707Z" }, + { url = "https://files.pythonhosted.org/packages/7e/d4/300fc921dff243cd518c7db3a4c614b7e4b2431b0d1145c1e274fd99bd70/coverage-7.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778", size = 206983, upload-time = "2024-08-04T19:43:49.082Z" }, + { url = "https://files.pythonhosted.org/packages/e1/ab/6bf00de5327ecb8db205f9ae596885417a31535eeda6e7b99463108782e1/coverage-7.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391", size = 207221, upload-time = "2024-08-04T19:43:52.15Z" }, + { url = "https://files.pythonhosted.org/packages/92/8f/2ead05e735022d1a7f3a0a683ac7f737de14850395a826192f0288703472/coverage-7.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8", size = 240342, upload-time = "2024-08-04T19:43:53.746Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ef/94043e478201ffa85b8ae2d2c79b4081e5a1b73438aafafccf3e9bafb6b5/coverage-7.6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d", size = 237371, upload-time = "2024-08-04T19:43:55.993Z" }, + { url = "https://files.pythonhosted.org/packages/1f/0f/c890339dd605f3ebc269543247bdd43b703cce6825b5ed42ff5f2d6122c7/coverage-7.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca", size = 239455, upload-time = "2024-08-04T19:43:57.618Z" }, + { url = "https://files.pythonhosted.org/packages/d1/04/7fd7b39ec7372a04efb0f70c70e35857a99b6a9188b5205efb4c77d6a57a/coverage-7.6.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163", size = 238924, upload-time = "2024-08-04T19:44:00.012Z" }, + { url = "https://files.pythonhosted.org/packages/ed/bf/73ce346a9d32a09cf369f14d2a06651329c984e106f5992c89579d25b27e/coverage-7.6.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a", size = 237252, upload-time = "2024-08-04T19:44:01.713Z" }, + { url = "https://files.pythonhosted.org/packages/86/74/1dc7a20969725e917b1e07fe71a955eb34bc606b938316bcc799f228374b/coverage-7.6.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d", size = 238897, upload-time = "2024-08-04T19:44:03.898Z" }, + { url = "https://files.pythonhosted.org/packages/b6/e9/d9cc3deceb361c491b81005c668578b0dfa51eed02cd081620e9a62f24ec/coverage-7.6.1-cp312-cp312-win32.whl", hash = "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5", size = 209606, upload-time = "2024-08-04T19:44:05.532Z" }, + { url = "https://files.pythonhosted.org/packages/47/c8/5a2e41922ea6740f77d555c4d47544acd7dc3f251fe14199c09c0f5958d3/coverage-7.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb", size = 210373, upload-time = "2024-08-04T19:44:07.079Z" }, + { url = "https://files.pythonhosted.org/packages/8c/f9/9aa4dfb751cb01c949c990d136a0f92027fbcc5781c6e921df1cb1563f20/coverage-7.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106", size = 207007, upload-time = "2024-08-04T19:44:09.453Z" }, + { url = "https://files.pythonhosted.org/packages/b9/67/e1413d5a8591622a46dd04ff80873b04c849268831ed5c304c16433e7e30/coverage-7.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9", size = 207269, upload-time = "2024-08-04T19:44:11.045Z" }, + { url = "https://files.pythonhosted.org/packages/14/5b/9dec847b305e44a5634d0fb8498d135ab1d88330482b74065fcec0622224/coverage-7.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c", size = 239886, upload-time = "2024-08-04T19:44:12.83Z" }, + { url = "https://files.pythonhosted.org/packages/7b/b7/35760a67c168e29f454928f51f970342d23cf75a2bb0323e0f07334c85f3/coverage-7.6.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a", size = 237037, upload-time = "2024-08-04T19:44:15.393Z" }, + { url = "https://files.pythonhosted.org/packages/f7/95/d2fd31f1d638df806cae59d7daea5abf2b15b5234016a5ebb502c2f3f7ee/coverage-7.6.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060", size = 239038, upload-time = "2024-08-04T19:44:17.466Z" }, + { url = "https://files.pythonhosted.org/packages/6e/bd/110689ff5752b67924efd5e2aedf5190cbbe245fc81b8dec1abaffba619d/coverage-7.6.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862", size = 238690, upload-time = "2024-08-04T19:44:19.336Z" }, + { url = "https://files.pythonhosted.org/packages/d3/a8/08d7b38e6ff8df52331c83130d0ab92d9c9a8b5462f9e99c9f051a4ae206/coverage-7.6.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388", size = 236765, upload-time = "2024-08-04T19:44:20.994Z" }, + { url = "https://files.pythonhosted.org/packages/d6/6a/9cf96839d3147d55ae713eb2d877f4d777e7dc5ba2bce227167d0118dfe8/coverage-7.6.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155", size = 238611, upload-time = "2024-08-04T19:44:22.616Z" }, + { url = "https://files.pythonhosted.org/packages/74/e4/7ff20d6a0b59eeaab40b3140a71e38cf52547ba21dbcf1d79c5a32bba61b/coverage-7.6.1-cp313-cp313-win32.whl", hash = "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a", size = 209671, upload-time = "2024-08-04T19:44:24.418Z" }, + { url = "https://files.pythonhosted.org/packages/35/59/1812f08a85b57c9fdb6d0b383d779e47b6f643bc278ed682859512517e83/coverage-7.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129", size = 210368, upload-time = "2024-08-04T19:44:26.276Z" }, + { url = "https://files.pythonhosted.org/packages/9c/15/08913be1c59d7562a3e39fce20661a98c0a3f59d5754312899acc6cb8a2d/coverage-7.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e", size = 207758, upload-time = "2024-08-04T19:44:29.028Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ae/b5d58dff26cade02ada6ca612a76447acd69dccdbb3a478e9e088eb3d4b9/coverage-7.6.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962", size = 208035, upload-time = "2024-08-04T19:44:30.673Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d7/62095e355ec0613b08dfb19206ce3033a0eedb6f4a67af5ed267a8800642/coverage-7.6.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb", size = 250839, upload-time = "2024-08-04T19:44:32.412Z" }, + { url = "https://files.pythonhosted.org/packages/7c/1e/c2967cb7991b112ba3766df0d9c21de46b476d103e32bb401b1b2adf3380/coverage-7.6.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704", size = 246569, upload-time = "2024-08-04T19:44:34.547Z" }, + { url = "https://files.pythonhosted.org/packages/8b/61/a7a6a55dd266007ed3b1df7a3386a0d760d014542d72f7c2c6938483b7bd/coverage-7.6.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b", size = 248927, upload-time = "2024-08-04T19:44:36.313Z" }, + { url = "https://files.pythonhosted.org/packages/c8/fa/13a6f56d72b429f56ef612eb3bc5ce1b75b7ee12864b3bd12526ab794847/coverage-7.6.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f", size = 248401, upload-time = "2024-08-04T19:44:38.155Z" }, + { url = "https://files.pythonhosted.org/packages/75/06/0429c652aa0fb761fc60e8c6b291338c9173c6aa0f4e40e1902345b42830/coverage-7.6.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223", size = 246301, upload-time = "2024-08-04T19:44:39.883Z" }, + { url = "https://files.pythonhosted.org/packages/52/76/1766bb8b803a88f93c3a2d07e30ffa359467810e5cbc68e375ebe6906efb/coverage-7.6.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3", size = 247598, upload-time = "2024-08-04T19:44:41.59Z" }, + { url = "https://files.pythonhosted.org/packages/66/8b/f54f8db2ae17188be9566e8166ac6df105c1c611e25da755738025708d54/coverage-7.6.1-cp313-cp313t-win32.whl", hash = "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f", size = 210307, upload-time = "2024-08-04T19:44:43.301Z" }, + { url = "https://files.pythonhosted.org/packages/9f/b0/e0dca6da9170aefc07515cce067b97178cefafb512d00a87a1c717d2efd5/coverage-7.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657", size = 211453, upload-time = "2024-08-04T19:44:45.677Z" }, + { url = "https://files.pythonhosted.org/packages/81/d0/d9e3d554e38beea5a2e22178ddb16587dbcbe9a1ef3211f55733924bf7fa/coverage-7.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6db04803b6c7291985a761004e9060b2bca08da6d04f26a7f2294b8623a0c1a0", size = 206674, upload-time = "2024-08-04T19:44:47.694Z" }, + { url = "https://files.pythonhosted.org/packages/38/ea/cab2dc248d9f45b2b7f9f1f596a4d75a435cb364437c61b51d2eb33ceb0e/coverage-7.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f1adfc8ac319e1a348af294106bc6a8458a0f1633cc62a1446aebc30c5fa186a", size = 207101, upload-time = "2024-08-04T19:44:49.32Z" }, + { url = "https://files.pythonhosted.org/packages/ca/6f/f82f9a500c7c5722368978a5390c418d2a4d083ef955309a8748ecaa8920/coverage-7.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a95324a9de9650a729239daea117df21f4b9868ce32e63f8b650ebe6cef5595b", size = 236554, upload-time = "2024-08-04T19:44:51.631Z" }, + { url = "https://files.pythonhosted.org/packages/a6/94/d3055aa33d4e7e733d8fa309d9adf147b4b06a82c1346366fc15a2b1d5fa/coverage-7.6.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b43c03669dc4618ec25270b06ecd3ee4fa94c7f9b3c14bae6571ca00ef98b0d3", size = 234440, upload-time = "2024-08-04T19:44:53.464Z" }, + { url = "https://files.pythonhosted.org/packages/e4/6e/885bcd787d9dd674de4a7d8ec83faf729534c63d05d51d45d4fa168f7102/coverage-7.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8929543a7192c13d177b770008bc4e8119f2e1f881d563fc6b6305d2d0ebe9de", size = 235889, upload-time = "2024-08-04T19:44:55.165Z" }, + { url = "https://files.pythonhosted.org/packages/f4/63/df50120a7744492710854860783d6819ff23e482dee15462c9a833cc428a/coverage-7.6.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:a09ece4a69cf399510c8ab25e0950d9cf2b42f7b3cb0374f95d2e2ff594478a6", size = 235142, upload-time = "2024-08-04T19:44:57.269Z" }, + { url = "https://files.pythonhosted.org/packages/3a/5d/9d0acfcded2b3e9ce1c7923ca52ccc00c78a74e112fc2aee661125b7843b/coverage-7.6.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9054a0754de38d9dbd01a46621636689124d666bad1936d76c0341f7d71bf569", size = 233805, upload-time = "2024-08-04T19:44:59.033Z" }, + { url = "https://files.pythonhosted.org/packages/c4/56/50abf070cb3cd9b1dd32f2c88f083aab561ecbffbcd783275cb51c17f11d/coverage-7.6.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0dbde0f4aa9a16fa4d754356a8f2e36296ff4d83994b2c9d8398aa32f222f989", size = 234655, upload-time = "2024-08-04T19:45:01.398Z" }, + { url = "https://files.pythonhosted.org/packages/25/ee/b4c246048b8485f85a2426ef4abab88e48c6e80c74e964bea5cd4cd4b115/coverage-7.6.1-cp38-cp38-win32.whl", hash = "sha256:da511e6ad4f7323ee5702e6633085fb76c2f893aaf8ce4c51a0ba4fc07580ea7", size = 209296, upload-time = "2024-08-04T19:45:03.819Z" }, + { url = "https://files.pythonhosted.org/packages/5c/1c/96cf86b70b69ea2b12924cdf7cabb8ad10e6130eab8d767a1099fbd2a44f/coverage-7.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:3f1156e3e8f2872197af3840d8ad307a9dd18e615dc64d9ee41696f287c57ad8", size = 210137, upload-time = "2024-08-04T19:45:06.25Z" }, + { url = "https://files.pythonhosted.org/packages/19/d3/d54c5aa83268779d54c86deb39c1c4566e5d45c155369ca152765f8db413/coverage-7.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255", size = 206688, upload-time = "2024-08-04T19:45:08.358Z" }, + { url = "https://files.pythonhosted.org/packages/a5/fe/137d5dca72e4a258b1bc17bb04f2e0196898fe495843402ce826a7419fe3/coverage-7.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8", size = 207120, upload-time = "2024-08-04T19:45:11.526Z" }, + { url = "https://files.pythonhosted.org/packages/78/5b/a0a796983f3201ff5485323b225d7c8b74ce30c11f456017e23d8e8d1945/coverage-7.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2", size = 235249, upload-time = "2024-08-04T19:45:13.202Z" }, + { url = "https://files.pythonhosted.org/packages/4e/e1/76089d6a5ef9d68f018f65411fcdaaeb0141b504587b901d74e8587606ad/coverage-7.6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e0b2df163b8ed01d515807af24f63de04bebcecbd6c3bfeff88385789fdf75a", size = 233237, upload-time = "2024-08-04T19:45:14.961Z" }, + { url = "https://files.pythonhosted.org/packages/9a/6f/eef79b779a540326fee9520e5542a8b428cc3bfa8b7c8f1022c1ee4fc66c/coverage-7.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:609b06f178fe8e9f89ef676532760ec0b4deea15e9969bf754b37f7c40326dbc", size = 234311, upload-time = "2024-08-04T19:45:16.924Z" }, + { url = "https://files.pythonhosted.org/packages/75/e1/656d65fb126c29a494ef964005702b012f3498db1a30dd562958e85a4049/coverage-7.6.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:702855feff378050ae4f741045e19a32d57d19f3e0676d589df0575008ea5004", size = 233453, upload-time = "2024-08-04T19:45:18.672Z" }, + { url = "https://files.pythonhosted.org/packages/68/6a/45f108f137941a4a1238c85f28fd9d048cc46b5466d6b8dda3aba1bb9d4f/coverage-7.6.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2bdb062ea438f22d99cba0d7829c2ef0af1d768d1e4a4f528087224c90b132cb", size = 231958, upload-time = "2024-08-04T19:45:20.63Z" }, + { url = "https://files.pythonhosted.org/packages/9b/e7/47b809099168b8b8c72ae311efc3e88c8d8a1162b3ba4b8da3cfcdb85743/coverage-7.6.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9c56863d44bd1c4fe2abb8a4d6f5371d197f1ac0ebdee542f07f35895fc07f36", size = 232938, upload-time = "2024-08-04T19:45:23.062Z" }, + { url = "https://files.pythonhosted.org/packages/52/80/052222ba7058071f905435bad0ba392cc12006380731c37afaf3fe749b88/coverage-7.6.1-cp39-cp39-win32.whl", hash = "sha256:6e2cd258d7d927d09493c8df1ce9174ad01b381d4729a9d8d4e38670ca24774c", size = 209352, upload-time = "2024-08-04T19:45:25.042Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d8/1b92e0b3adcf384e98770a00ca095da1b5f7b483e6563ae4eb5e935d24a1/coverage-7.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca", size = 210153, upload-time = "2024-08-04T19:45:27.079Z" }, + { url = "https://files.pythonhosted.org/packages/a5/2b/0354ed096bca64dc8e32a7cbcae28b34cb5ad0b1fe2125d6d99583313ac0/coverage-7.6.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df", size = 198926, upload-time = "2024-08-04T19:45:28.875Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version < '3.9'" }, +] + +[[package]] +name = "coverage" +version = "7.10.7" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/51/26/d22c300112504f5f9a9fd2297ce33c35f3d353e4aeb987c8419453b2a7c2/coverage-7.10.7.tar.gz", hash = "sha256:f4ab143ab113be368a3e9b795f9cd7906c5ef407d6173fe9675a902e1fffc239", size = 827704, upload-time = "2025-09-21T20:03:56.815Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/6c/3a3f7a46888e69d18abe3ccc6fe4cb16cccb1e6a2f99698931dafca489e6/coverage-7.10.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fc04cc7a3db33664e0c2d10eb8990ff6b3536f6842c9590ae8da4c614b9ed05a", size = 217987, upload-time = "2025-09-21T20:00:57.218Z" }, + { url = "https://files.pythonhosted.org/packages/03/94/952d30f180b1a916c11a56f5c22d3535e943aa22430e9e3322447e520e1c/coverage-7.10.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e201e015644e207139f7e2351980feb7040e6f4b2c2978892f3e3789d1c125e5", size = 218388, upload-time = "2025-09-21T20:01:00.081Z" }, + { url = "https://files.pythonhosted.org/packages/50/2b/9e0cf8ded1e114bcd8b2fd42792b57f1c4e9e4ea1824cde2af93a67305be/coverage-7.10.7-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:240af60539987ced2c399809bd34f7c78e8abe0736af91c3d7d0e795df633d17", size = 245148, upload-time = "2025-09-21T20:01:01.768Z" }, + { url = "https://files.pythonhosted.org/packages/19/20/d0384ac06a6f908783d9b6aa6135e41b093971499ec488e47279f5b846e6/coverage-7.10.7-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8421e088bc051361b01c4b3a50fd39a4b9133079a2229978d9d30511fd05231b", size = 246958, upload-time = "2025-09-21T20:01:03.355Z" }, + { url = "https://files.pythonhosted.org/packages/60/83/5c283cff3d41285f8eab897651585db908a909c572bdc014bcfaf8a8b6ae/coverage-7.10.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6be8ed3039ae7f7ac5ce058c308484787c86e8437e72b30bf5e88b8ea10f3c87", size = 248819, upload-time = "2025-09-21T20:01:04.968Z" }, + { url = "https://files.pythonhosted.org/packages/60/22/02eb98fdc5ff79f423e990d877693e5310ae1eab6cb20ae0b0b9ac45b23b/coverage-7.10.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e28299d9f2e889e6d51b1f043f58d5f997c373cc12e6403b90df95b8b047c13e", size = 245754, upload-time = "2025-09-21T20:01:06.321Z" }, + { url = "https://files.pythonhosted.org/packages/b4/bc/25c83bcf3ad141b32cd7dc45485ef3c01a776ca3aa8ef0a93e77e8b5bc43/coverage-7.10.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c4e16bd7761c5e454f4efd36f345286d6f7c5fa111623c355691e2755cae3b9e", size = 246860, upload-time = "2025-09-21T20:01:07.605Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b7/95574702888b58c0928a6e982038c596f9c34d52c5e5107f1eef729399b5/coverage-7.10.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b1c81d0e5e160651879755c9c675b974276f135558cf4ba79fee7b8413a515df", size = 244877, upload-time = "2025-09-21T20:01:08.829Z" }, + { url = "https://files.pythonhosted.org/packages/47/b6/40095c185f235e085df0e0b158f6bd68cc6e1d80ba6c7721dc81d97ec318/coverage-7.10.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:606cc265adc9aaedcc84f1f064f0e8736bc45814f15a357e30fca7ecc01504e0", size = 245108, upload-time = "2025-09-21T20:01:10.527Z" }, + { url = "https://files.pythonhosted.org/packages/c8/50/4aea0556da7a4b93ec9168420d170b55e2eb50ae21b25062513d020c6861/coverage-7.10.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:10b24412692df990dbc34f8fb1b6b13d236ace9dfdd68df5b28c2e39cafbba13", size = 245752, upload-time = "2025-09-21T20:01:11.857Z" }, + { url = "https://files.pythonhosted.org/packages/6a/28/ea1a84a60828177ae3b100cb6723838523369a44ec5742313ed7db3da160/coverage-7.10.7-cp310-cp310-win32.whl", hash = "sha256:b51dcd060f18c19290d9b8a9dd1e0181538df2ce0717f562fff6cf74d9fc0b5b", size = 220497, upload-time = "2025-09-21T20:01:13.459Z" }, + { url = "https://files.pythonhosted.org/packages/fc/1a/a81d46bbeb3c3fd97b9602ebaa411e076219a150489bcc2c025f151bd52d/coverage-7.10.7-cp310-cp310-win_amd64.whl", hash = "sha256:3a622ac801b17198020f09af3eaf45666b344a0d69fc2a6ffe2ea83aeef1d807", size = 221392, upload-time = "2025-09-21T20:01:14.722Z" }, + { url = "https://files.pythonhosted.org/packages/d2/5d/c1a17867b0456f2e9ce2d8d4708a4c3a089947d0bec9c66cdf60c9e7739f/coverage-7.10.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a609f9c93113be646f44c2a0256d6ea375ad047005d7f57a5c15f614dc1b2f59", size = 218102, upload-time = "2025-09-21T20:01:16.089Z" }, + { url = "https://files.pythonhosted.org/packages/54/f0/514dcf4b4e3698b9a9077f084429681bf3aad2b4a72578f89d7f643eb506/coverage-7.10.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:65646bb0359386e07639c367a22cf9b5bf6304e8630b565d0626e2bdf329227a", size = 218505, upload-time = "2025-09-21T20:01:17.788Z" }, + { url = "https://files.pythonhosted.org/packages/20/f6/9626b81d17e2a4b25c63ac1b425ff307ecdeef03d67c9a147673ae40dc36/coverage-7.10.7-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5f33166f0dfcce728191f520bd2692914ec70fac2713f6bf3ce59c3deacb4699", size = 248898, upload-time = "2025-09-21T20:01:19.488Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ef/bd8e719c2f7417ba03239052e099b76ea1130ac0cbb183ee1fcaa58aaff3/coverage-7.10.7-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:35f5e3f9e455bb17831876048355dca0f758b6df22f49258cb5a91da23ef437d", size = 250831, upload-time = "2025-09-21T20:01:20.817Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b6/bf054de41ec948b151ae2b79a55c107f5760979538f5fb80c195f2517718/coverage-7.10.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4da86b6d62a496e908ac2898243920c7992499c1712ff7c2b6d837cc69d9467e", size = 252937, upload-time = "2025-09-21T20:01:22.171Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e5/3860756aa6f9318227443c6ce4ed7bf9e70bb7f1447a0353f45ac5c7974b/coverage-7.10.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6b8b09c1fad947c84bbbc95eca841350fad9cbfa5a2d7ca88ac9f8d836c92e23", size = 249021, upload-time = "2025-09-21T20:01:23.907Z" }, + { url = "https://files.pythonhosted.org/packages/26/0f/bd08bd042854f7fd07b45808927ebcce99a7ed0f2f412d11629883517ac2/coverage-7.10.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4376538f36b533b46f8971d3a3e63464f2c7905c9800db97361c43a2b14792ab", size = 250626, upload-time = "2025-09-21T20:01:25.721Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a7/4777b14de4abcc2e80c6b1d430f5d51eb18ed1d75fca56cbce5f2db9b36e/coverage-7.10.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:121da30abb574f6ce6ae09840dae322bef734480ceafe410117627aa54f76d82", size = 248682, upload-time = "2025-09-21T20:01:27.105Z" }, + { url = "https://files.pythonhosted.org/packages/34/72/17d082b00b53cd45679bad682fac058b87f011fd8b9fe31d77f5f8d3a4e4/coverage-7.10.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:88127d40df529336a9836870436fc2751c339fbaed3a836d42c93f3e4bd1d0a2", size = 248402, upload-time = "2025-09-21T20:01:28.629Z" }, + { url = "https://files.pythonhosted.org/packages/81/7a/92367572eb5bdd6a84bfa278cc7e97db192f9f45b28c94a9ca1a921c3577/coverage-7.10.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ba58bbcd1b72f136080c0bccc2400d66cc6115f3f906c499013d065ac33a4b61", size = 249320, upload-time = "2025-09-21T20:01:30.004Z" }, + { url = "https://files.pythonhosted.org/packages/2f/88/a23cc185f6a805dfc4fdf14a94016835eeb85e22ac3a0e66d5e89acd6462/coverage-7.10.7-cp311-cp311-win32.whl", hash = "sha256:972b9e3a4094b053a4e46832b4bc829fc8a8d347160eb39d03f1690316a99c14", size = 220536, upload-time = "2025-09-21T20:01:32.184Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ef/0b510a399dfca17cec7bc2f05ad8bd78cf55f15c8bc9a73ab20c5c913c2e/coverage-7.10.7-cp311-cp311-win_amd64.whl", hash = "sha256:a7b55a944a7f43892e28ad4bc0561dfd5f0d73e605d1aa5c3c976b52aea121d2", size = 221425, upload-time = "2025-09-21T20:01:33.557Z" }, + { url = "https://files.pythonhosted.org/packages/51/7f/023657f301a276e4ba1850f82749bc136f5a7e8768060c2e5d9744a22951/coverage-7.10.7-cp311-cp311-win_arm64.whl", hash = "sha256:736f227fb490f03c6488f9b6d45855f8e0fd749c007f9303ad30efab0e73c05a", size = 220103, upload-time = "2025-09-21T20:01:34.929Z" }, + { url = "https://files.pythonhosted.org/packages/13/e4/eb12450f71b542a53972d19117ea5a5cea1cab3ac9e31b0b5d498df1bd5a/coverage-7.10.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7bb3b9ddb87ef7725056572368040c32775036472d5a033679d1fa6c8dc08417", size = 218290, upload-time = "2025-09-21T20:01:36.455Z" }, + { url = "https://files.pythonhosted.org/packages/37/66/593f9be12fc19fb36711f19a5371af79a718537204d16ea1d36f16bd78d2/coverage-7.10.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:18afb24843cbc175687225cab1138c95d262337f5473512010e46831aa0c2973", size = 218515, upload-time = "2025-09-21T20:01:37.982Z" }, + { url = "https://files.pythonhosted.org/packages/66/80/4c49f7ae09cafdacc73fbc30949ffe77359635c168f4e9ff33c9ebb07838/coverage-7.10.7-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:399a0b6347bcd3822be369392932884b8216d0944049ae22925631a9b3d4ba4c", size = 250020, upload-time = "2025-09-21T20:01:39.617Z" }, + { url = "https://files.pythonhosted.org/packages/a6/90/a64aaacab3b37a17aaedd83e8000142561a29eb262cede42d94a67f7556b/coverage-7.10.7-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:314f2c326ded3f4b09be11bc282eb2fc861184bc95748ae67b360ac962770be7", size = 252769, upload-time = "2025-09-21T20:01:41.341Z" }, + { url = "https://files.pythonhosted.org/packages/98/2e/2dda59afd6103b342e096f246ebc5f87a3363b5412609946c120f4e7750d/coverage-7.10.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c41e71c9cfb854789dee6fc51e46743a6d138b1803fab6cb860af43265b42ea6", size = 253901, upload-time = "2025-09-21T20:01:43.042Z" }, + { url = "https://files.pythonhosted.org/packages/53/dc/8d8119c9051d50f3119bb4a75f29f1e4a6ab9415cd1fa8bf22fcc3fb3b5f/coverage-7.10.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc01f57ca26269c2c706e838f6422e2a8788e41b3e3c65e2f41148212e57cd59", size = 250413, upload-time = "2025-09-21T20:01:44.469Z" }, + { url = "https://files.pythonhosted.org/packages/98/b3/edaff9c5d79ee4d4b6d3fe046f2b1d799850425695b789d491a64225d493/coverage-7.10.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a6442c59a8ac8b85812ce33bc4d05bde3fb22321fa8294e2a5b487c3505f611b", size = 251820, upload-time = "2025-09-21T20:01:45.915Z" }, + { url = "https://files.pythonhosted.org/packages/11/25/9a0728564bb05863f7e513e5a594fe5ffef091b325437f5430e8cfb0d530/coverage-7.10.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:78a384e49f46b80fb4c901d52d92abe098e78768ed829c673fbb53c498bef73a", size = 249941, upload-time = "2025-09-21T20:01:47.296Z" }, + { url = "https://files.pythonhosted.org/packages/e0/fd/ca2650443bfbef5b0e74373aac4df67b08180d2f184b482c41499668e258/coverage-7.10.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5e1e9802121405ede4b0133aa4340ad8186a1d2526de5b7c3eca519db7bb89fb", size = 249519, upload-time = "2025-09-21T20:01:48.73Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/f692f125fb4299b6f963b0745124998ebb8e73ecdfce4ceceb06a8c6bec5/coverage-7.10.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d41213ea25a86f69efd1575073d34ea11aabe075604ddf3d148ecfec9e1e96a1", size = 251375, upload-time = "2025-09-21T20:01:50.529Z" }, + { url = "https://files.pythonhosted.org/packages/5e/75/61b9bbd6c7d24d896bfeec57acba78e0f8deac68e6baf2d4804f7aae1f88/coverage-7.10.7-cp312-cp312-win32.whl", hash = "sha256:77eb4c747061a6af8d0f7bdb31f1e108d172762ef579166ec84542f711d90256", size = 220699, upload-time = "2025-09-21T20:01:51.941Z" }, + { url = "https://files.pythonhosted.org/packages/ca/f3/3bf7905288b45b075918d372498f1cf845b5b579b723c8fd17168018d5f5/coverage-7.10.7-cp312-cp312-win_amd64.whl", hash = "sha256:f51328ffe987aecf6d09f3cd9d979face89a617eacdaea43e7b3080777f647ba", size = 221512, upload-time = "2025-09-21T20:01:53.481Z" }, + { url = "https://files.pythonhosted.org/packages/5c/44/3e32dbe933979d05cf2dac5e697c8599cfe038aaf51223ab901e208d5a62/coverage-7.10.7-cp312-cp312-win_arm64.whl", hash = "sha256:bda5e34f8a75721c96085903c6f2197dc398c20ffd98df33f866a9c8fd95f4bf", size = 220147, upload-time = "2025-09-21T20:01:55.2Z" }, + { url = "https://files.pythonhosted.org/packages/9a/94/b765c1abcb613d103b64fcf10395f54d69b0ef8be6a0dd9c524384892cc7/coverage-7.10.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:981a651f543f2854abd3b5fcb3263aac581b18209be49863ba575de6edf4c14d", size = 218320, upload-time = "2025-09-21T20:01:56.629Z" }, + { url = "https://files.pythonhosted.org/packages/72/4f/732fff31c119bb73b35236dd333030f32c4bfe909f445b423e6c7594f9a2/coverage-7.10.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:73ab1601f84dc804f7812dc297e93cd99381162da39c47040a827d4e8dafe63b", size = 218575, upload-time = "2025-09-21T20:01:58.203Z" }, + { url = "https://files.pythonhosted.org/packages/87/02/ae7e0af4b674be47566707777db1aa375474f02a1d64b9323e5813a6cdd5/coverage-7.10.7-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a8b6f03672aa6734e700bbcd65ff050fd19cddfec4b031cc8cf1c6967de5a68e", size = 249568, upload-time = "2025-09-21T20:01:59.748Z" }, + { url = "https://files.pythonhosted.org/packages/a2/77/8c6d22bf61921a59bce5471c2f1f7ac30cd4ac50aadde72b8c48d5727902/coverage-7.10.7-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10b6ba00ab1132a0ce4428ff68cf50a25efd6840a42cdf4239c9b99aad83be8b", size = 252174, upload-time = "2025-09-21T20:02:01.192Z" }, + { url = "https://files.pythonhosted.org/packages/b1/20/b6ea4f69bbb52dac0aebd62157ba6a9dddbfe664f5af8122dac296c3ee15/coverage-7.10.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c79124f70465a150e89340de5963f936ee97097d2ef76c869708c4248c63ca49", size = 253447, upload-time = "2025-09-21T20:02:02.701Z" }, + { url = "https://files.pythonhosted.org/packages/f9/28/4831523ba483a7f90f7b259d2018fef02cb4d5b90bc7c1505d6e5a84883c/coverage-7.10.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:69212fbccdbd5b0e39eac4067e20a4a5256609e209547d86f740d68ad4f04911", size = 249779, upload-time = "2025-09-21T20:02:04.185Z" }, + { url = "https://files.pythonhosted.org/packages/a7/9f/4331142bc98c10ca6436d2d620c3e165f31e6c58d43479985afce6f3191c/coverage-7.10.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7ea7c6c9d0d286d04ed3541747e6597cbe4971f22648b68248f7ddcd329207f0", size = 251604, upload-time = "2025-09-21T20:02:06.034Z" }, + { url = "https://files.pythonhosted.org/packages/ce/60/bda83b96602036b77ecf34e6393a3836365481b69f7ed7079ab85048202b/coverage-7.10.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b9be91986841a75042b3e3243d0b3cb0b2434252b977baaf0cd56e960fe1e46f", size = 249497, upload-time = "2025-09-21T20:02:07.619Z" }, + { url = "https://files.pythonhosted.org/packages/5f/af/152633ff35b2af63977edd835d8e6430f0caef27d171edf2fc76c270ef31/coverage-7.10.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b281d5eca50189325cfe1f365fafade89b14b4a78d9b40b05ddd1fc7d2a10a9c", size = 249350, upload-time = "2025-09-21T20:02:10.34Z" }, + { url = "https://files.pythonhosted.org/packages/9d/71/d92105d122bd21cebba877228990e1646d862e34a98bb3374d3fece5a794/coverage-7.10.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:99e4aa63097ab1118e75a848a28e40d68b08a5e19ce587891ab7fd04475e780f", size = 251111, upload-time = "2025-09-21T20:02:12.122Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9e/9fdb08f4bf476c912f0c3ca292e019aab6712c93c9344a1653986c3fd305/coverage-7.10.7-cp313-cp313-win32.whl", hash = "sha256:dc7c389dce432500273eaf48f410b37886be9208b2dd5710aaf7c57fd442c698", size = 220746, upload-time = "2025-09-21T20:02:13.919Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b1/a75fd25df44eab52d1931e89980d1ada46824c7a3210be0d3c88a44aaa99/coverage-7.10.7-cp313-cp313-win_amd64.whl", hash = "sha256:cac0fdca17b036af3881a9d2729a850b76553f3f716ccb0360ad4dbc06b3b843", size = 221541, upload-time = "2025-09-21T20:02:15.57Z" }, + { url = "https://files.pythonhosted.org/packages/14/3a/d720d7c989562a6e9a14b2c9f5f2876bdb38e9367126d118495b89c99c37/coverage-7.10.7-cp313-cp313-win_arm64.whl", hash = "sha256:4b6f236edf6e2f9ae8fcd1332da4e791c1b6ba0dc16a2dc94590ceccb482e546", size = 220170, upload-time = "2025-09-21T20:02:17.395Z" }, + { url = "https://files.pythonhosted.org/packages/bb/22/e04514bf2a735d8b0add31d2b4ab636fc02370730787c576bb995390d2d5/coverage-7.10.7-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a0ec07fd264d0745ee396b666d47cef20875f4ff2375d7c4f58235886cc1ef0c", size = 219029, upload-time = "2025-09-21T20:02:18.936Z" }, + { url = "https://files.pythonhosted.org/packages/11/0b/91128e099035ece15da3445d9015e4b4153a6059403452d324cbb0a575fa/coverage-7.10.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd5e856ebb7bfb7672b0086846db5afb4567a7b9714b8a0ebafd211ec7ce6a15", size = 219259, upload-time = "2025-09-21T20:02:20.44Z" }, + { url = "https://files.pythonhosted.org/packages/8b/51/66420081e72801536a091a0c8f8c1f88a5c4bf7b9b1bdc6222c7afe6dc9b/coverage-7.10.7-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f57b2a3c8353d3e04acf75b3fed57ba41f5c0646bbf1d10c7c282291c97936b4", size = 260592, upload-time = "2025-09-21T20:02:22.313Z" }, + { url = "https://files.pythonhosted.org/packages/5d/22/9b8d458c2881b22df3db5bb3e7369e63d527d986decb6c11a591ba2364f7/coverage-7.10.7-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1ef2319dd15a0b009667301a3f84452a4dc6fddfd06b0c5c53ea472d3989fbf0", size = 262768, upload-time = "2025-09-21T20:02:24.287Z" }, + { url = "https://files.pythonhosted.org/packages/f7/08/16bee2c433e60913c610ea200b276e8eeef084b0d200bdcff69920bd5828/coverage-7.10.7-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83082a57783239717ceb0ad584de3c69cf581b2a95ed6bf81ea66034f00401c0", size = 264995, upload-time = "2025-09-21T20:02:26.133Z" }, + { url = "https://files.pythonhosted.org/packages/20/9d/e53eb9771d154859b084b90201e5221bca7674ba449a17c101a5031d4054/coverage-7.10.7-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:50aa94fb1fb9a397eaa19c0d5ec15a5edd03a47bf1a3a6111a16b36e190cff65", size = 259546, upload-time = "2025-09-21T20:02:27.716Z" }, + { url = "https://files.pythonhosted.org/packages/ad/b0/69bc7050f8d4e56a89fb550a1577d5d0d1db2278106f6f626464067b3817/coverage-7.10.7-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2120043f147bebb41c85b97ac45dd173595ff14f2a584f2963891cbcc3091541", size = 262544, upload-time = "2025-09-21T20:02:29.216Z" }, + { url = "https://files.pythonhosted.org/packages/ef/4b/2514b060dbd1bc0aaf23b852c14bb5818f244c664cb16517feff6bb3a5ab/coverage-7.10.7-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2fafd773231dd0378fdba66d339f84904a8e57a262f583530f4f156ab83863e6", size = 260308, upload-time = "2025-09-21T20:02:31.226Z" }, + { url = "https://files.pythonhosted.org/packages/54/78/7ba2175007c246d75e496f64c06e94122bdb914790a1285d627a918bd271/coverage-7.10.7-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:0b944ee8459f515f28b851728ad224fa2d068f1513ef6b7ff1efafeb2185f999", size = 258920, upload-time = "2025-09-21T20:02:32.823Z" }, + { url = "https://files.pythonhosted.org/packages/c0/b3/fac9f7abbc841409b9a410309d73bfa6cfb2e51c3fada738cb607ce174f8/coverage-7.10.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4b583b97ab2e3efe1b3e75248a9b333bd3f8b0b1b8e5b45578e05e5850dfb2c2", size = 261434, upload-time = "2025-09-21T20:02:34.86Z" }, + { url = "https://files.pythonhosted.org/packages/ee/51/a03bec00d37faaa891b3ff7387192cef20f01604e5283a5fabc95346befa/coverage-7.10.7-cp313-cp313t-win32.whl", hash = "sha256:2a78cd46550081a7909b3329e2266204d584866e8d97b898cd7fb5ac8d888b1a", size = 221403, upload-time = "2025-09-21T20:02:37.034Z" }, + { url = "https://files.pythonhosted.org/packages/53/22/3cf25d614e64bf6d8e59c7c669b20d6d940bb337bdee5900b9ca41c820bb/coverage-7.10.7-cp313-cp313t-win_amd64.whl", hash = "sha256:33a5e6396ab684cb43dc7befa386258acb2d7fae7f67330ebb85ba4ea27938eb", size = 222469, upload-time = "2025-09-21T20:02:39.011Z" }, + { url = "https://files.pythonhosted.org/packages/49/a1/00164f6d30d8a01c3c9c48418a7a5be394de5349b421b9ee019f380df2a0/coverage-7.10.7-cp313-cp313t-win_arm64.whl", hash = "sha256:86b0e7308289ddde73d863b7683f596d8d21c7d8664ce1dee061d0bcf3fbb4bb", size = 220731, upload-time = "2025-09-21T20:02:40.939Z" }, + { url = "https://files.pythonhosted.org/packages/23/9c/5844ab4ca6a4dd97a1850e030a15ec7d292b5c5cb93082979225126e35dd/coverage-7.10.7-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b06f260b16ead11643a5a9f955bd4b5fd76c1a4c6796aeade8520095b75de520", size = 218302, upload-time = "2025-09-21T20:02:42.527Z" }, + { url = "https://files.pythonhosted.org/packages/f0/89/673f6514b0961d1f0e20ddc242e9342f6da21eaba3489901b565c0689f34/coverage-7.10.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:212f8f2e0612778f09c55dd4872cb1f64a1f2b074393d139278ce902064d5b32", size = 218578, upload-time = "2025-09-21T20:02:44.468Z" }, + { url = "https://files.pythonhosted.org/packages/05/e8/261cae479e85232828fb17ad536765c88dd818c8470aca690b0ac6feeaa3/coverage-7.10.7-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3445258bcded7d4aa630ab8296dea4d3f15a255588dd535f980c193ab6b95f3f", size = 249629, upload-time = "2025-09-21T20:02:46.503Z" }, + { url = "https://files.pythonhosted.org/packages/82/62/14ed6546d0207e6eda876434e3e8475a3e9adbe32110ce896c9e0c06bb9a/coverage-7.10.7-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb45474711ba385c46a0bfe696c695a929ae69ac636cda8f532be9e8c93d720a", size = 252162, upload-time = "2025-09-21T20:02:48.689Z" }, + { url = "https://files.pythonhosted.org/packages/ff/49/07f00db9ac6478e4358165a08fb41b469a1b053212e8a00cb02f0d27a05f/coverage-7.10.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:813922f35bd800dca9994c5971883cbc0d291128a5de6b167c7aa697fcf59360", size = 253517, upload-time = "2025-09-21T20:02:50.31Z" }, + { url = "https://files.pythonhosted.org/packages/a2/59/c5201c62dbf165dfbc91460f6dbbaa85a8b82cfa6131ac45d6c1bfb52deb/coverage-7.10.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:93c1b03552081b2a4423091d6fb3787265b8f86af404cff98d1b5342713bdd69", size = 249632, upload-time = "2025-09-21T20:02:51.971Z" }, + { url = "https://files.pythonhosted.org/packages/07/ae/5920097195291a51fb00b3a70b9bbd2edbfe3c84876a1762bd1ef1565ebc/coverage-7.10.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:cc87dd1b6eaf0b848eebb1c86469b9f72a1891cb42ac7adcfbce75eadb13dd14", size = 251520, upload-time = "2025-09-21T20:02:53.858Z" }, + { url = "https://files.pythonhosted.org/packages/b9/3c/a815dde77a2981f5743a60b63df31cb322c944843e57dbd579326625a413/coverage-7.10.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:39508ffda4f343c35f3236fe8d1a6634a51f4581226a1262769d7f970e73bffe", size = 249455, upload-time = "2025-09-21T20:02:55.807Z" }, + { url = "https://files.pythonhosted.org/packages/aa/99/f5cdd8421ea656abefb6c0ce92556709db2265c41e8f9fc6c8ae0f7824c9/coverage-7.10.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:925a1edf3d810537c5a3abe78ec5530160c5f9a26b1f4270b40e62cc79304a1e", size = 249287, upload-time = "2025-09-21T20:02:57.784Z" }, + { url = "https://files.pythonhosted.org/packages/c3/7a/e9a2da6a1fc5d007dd51fca083a663ab930a8c4d149c087732a5dbaa0029/coverage-7.10.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2c8b9a0636f94c43cd3576811e05b89aa9bc2d0a85137affc544ae5cb0e4bfbd", size = 250946, upload-time = "2025-09-21T20:02:59.431Z" }, + { url = "https://files.pythonhosted.org/packages/ef/5b/0b5799aa30380a949005a353715095d6d1da81927d6dbed5def2200a4e25/coverage-7.10.7-cp314-cp314-win32.whl", hash = "sha256:b7b8288eb7cdd268b0304632da8cb0bb93fadcfec2fe5712f7b9cc8f4d487be2", size = 221009, upload-time = "2025-09-21T20:03:01.324Z" }, + { url = "https://files.pythonhosted.org/packages/da/b0/e802fbb6eb746de006490abc9bb554b708918b6774b722bb3a0e6aa1b7de/coverage-7.10.7-cp314-cp314-win_amd64.whl", hash = "sha256:1ca6db7c8807fb9e755d0379ccc39017ce0a84dcd26d14b5a03b78563776f681", size = 221804, upload-time = "2025-09-21T20:03:03.4Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e8/71d0c8e374e31f39e3389bb0bd19e527d46f00ea8571ec7ec8fd261d8b44/coverage-7.10.7-cp314-cp314-win_arm64.whl", hash = "sha256:097c1591f5af4496226d5783d036bf6fd6cd0cbc132e071b33861de756efb880", size = 220384, upload-time = "2025-09-21T20:03:05.111Z" }, + { url = "https://files.pythonhosted.org/packages/62/09/9a5608d319fa3eba7a2019addeacb8c746fb50872b57a724c9f79f146969/coverage-7.10.7-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:a62c6ef0d50e6de320c270ff91d9dd0a05e7250cac2a800b7784bae474506e63", size = 219047, upload-time = "2025-09-21T20:03:06.795Z" }, + { url = "https://files.pythonhosted.org/packages/f5/6f/f58d46f33db9f2e3647b2d0764704548c184e6f5e014bef528b7f979ef84/coverage-7.10.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9fa6e4dd51fe15d8738708a973470f67a855ca50002294852e9571cdbd9433f2", size = 219266, upload-time = "2025-09-21T20:03:08.495Z" }, + { url = "https://files.pythonhosted.org/packages/74/5c/183ffc817ba68e0b443b8c934c8795553eb0c14573813415bd59941ee165/coverage-7.10.7-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8fb190658865565c549b6b4706856d6a7b09302c797eb2cf8e7fe9dabb043f0d", size = 260767, upload-time = "2025-09-21T20:03:10.172Z" }, + { url = "https://files.pythonhosted.org/packages/0f/48/71a8abe9c1ad7e97548835e3cc1adbf361e743e9d60310c5f75c9e7bf847/coverage-7.10.7-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:affef7c76a9ef259187ef31599a9260330e0335a3011732c4b9effa01e1cd6e0", size = 262931, upload-time = "2025-09-21T20:03:11.861Z" }, + { url = "https://files.pythonhosted.org/packages/84/fd/193a8fb132acfc0a901f72020e54be5e48021e1575bb327d8ee1097a28fd/coverage-7.10.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e16e07d85ca0cf8bafe5f5d23a0b850064e8e945d5677492b06bbe6f09cc699", size = 265186, upload-time = "2025-09-21T20:03:13.539Z" }, + { url = "https://files.pythonhosted.org/packages/b1/8f/74ecc30607dd95ad50e3034221113ccb1c6d4e8085cc761134782995daae/coverage-7.10.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:03ffc58aacdf65d2a82bbeb1ffe4d01ead4017a21bfd0454983b88ca73af94b9", size = 259470, upload-time = "2025-09-21T20:03:15.584Z" }, + { url = "https://files.pythonhosted.org/packages/0f/55/79ff53a769f20d71b07023ea115c9167c0bb56f281320520cf64c5298a96/coverage-7.10.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1b4fd784344d4e52647fd7857b2af5b3fbe6c239b0b5fa63e94eb67320770e0f", size = 262626, upload-time = "2025-09-21T20:03:17.673Z" }, + { url = "https://files.pythonhosted.org/packages/88/e2/dac66c140009b61ac3fc13af673a574b00c16efdf04f9b5c740703e953c0/coverage-7.10.7-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0ebbaddb2c19b71912c6f2518e791aa8b9f054985a0769bdb3a53ebbc765c6a1", size = 260386, upload-time = "2025-09-21T20:03:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/a2/f1/f48f645e3f33bb9ca8a496bc4a9671b52f2f353146233ebd7c1df6160440/coverage-7.10.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a2d9a3b260cc1d1dbdb1c582e63ddcf5363426a1a68faa0f5da28d8ee3c722a0", size = 258852, upload-time = "2025-09-21T20:03:21.007Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3b/8442618972c51a7affeead957995cfa8323c0c9bcf8fa5a027421f720ff4/coverage-7.10.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a3cc8638b2480865eaa3926d192e64ce6c51e3d29c849e09d5b4ad95efae5399", size = 261534, upload-time = "2025-09-21T20:03:23.12Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dc/101f3fa3a45146db0cb03f5b4376e24c0aac818309da23e2de0c75295a91/coverage-7.10.7-cp314-cp314t-win32.whl", hash = "sha256:67f8c5cbcd3deb7a60b3345dffc89a961a484ed0af1f6f73de91705cc6e31235", size = 221784, upload-time = "2025-09-21T20:03:24.769Z" }, + { url = "https://files.pythonhosted.org/packages/4c/a1/74c51803fc70a8a40d7346660379e144be772bab4ac7bb6e6b905152345c/coverage-7.10.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e1ed71194ef6dea7ed2d5cb5f7243d4bcd334bfb63e59878519be558078f848d", size = 222905, upload-time = "2025-09-21T20:03:26.93Z" }, + { url = "https://files.pythonhosted.org/packages/12/65/f116a6d2127df30bcafbceef0302d8a64ba87488bf6f73a6d8eebf060873/coverage-7.10.7-cp314-cp314t-win_arm64.whl", hash = "sha256:7fe650342addd8524ca63d77b2362b02345e5f1a093266787d210c70a50b471a", size = 220922, upload-time = "2025-09-21T20:03:28.672Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ad/d1c25053764b4c42eb294aae92ab617d2e4f803397f9c7c8295caa77a260/coverage-7.10.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fff7b9c3f19957020cac546c70025331113d2e61537f6e2441bc7657913de7d3", size = 217978, upload-time = "2025-09-21T20:03:30.362Z" }, + { url = "https://files.pythonhosted.org/packages/52/2f/b9f9daa39b80ece0b9548bbb723381e29bc664822d9a12c2135f8922c22b/coverage-7.10.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bc91b314cef27742da486d6839b677b3f2793dfe52b51bbbb7cf736d5c29281c", size = 218370, upload-time = "2025-09-21T20:03:32.147Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6e/30d006c3b469e58449650642383dddf1c8fb63d44fdf92994bfd46570695/coverage-7.10.7-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:567f5c155eda8df1d3d439d40a45a6a5f029b429b06648235f1e7e51b522b396", size = 244802, upload-time = "2025-09-21T20:03:33.919Z" }, + { url = "https://files.pythonhosted.org/packages/b0/49/8a070782ce7e6b94ff6a0b6d7c65ba6bc3091d92a92cef4cd4eb0767965c/coverage-7.10.7-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2af88deffcc8a4d5974cf2d502251bc3b2db8461f0b66d80a449c33757aa9f40", size = 246625, upload-time = "2025-09-21T20:03:36.09Z" }, + { url = "https://files.pythonhosted.org/packages/6a/92/1c1c5a9e8677ce56d42b97bdaca337b2d4d9ebe703d8c174ede52dbabd5f/coverage-7.10.7-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7315339eae3b24c2d2fa1ed7d7a38654cba34a13ef19fbcb9425da46d3dc594", size = 248399, upload-time = "2025-09-21T20:03:38.342Z" }, + { url = "https://files.pythonhosted.org/packages/c0/54/b140edee7257e815de7426d5d9846b58505dffc29795fff2dfb7f8a1c5a0/coverage-7.10.7-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:912e6ebc7a6e4adfdbb1aec371ad04c68854cd3bf3608b3514e7ff9062931d8a", size = 245142, upload-time = "2025-09-21T20:03:40.591Z" }, + { url = "https://files.pythonhosted.org/packages/e4/9e/6d6b8295940b118e8b7083b29226c71f6154f7ff41e9ca431f03de2eac0d/coverage-7.10.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f49a05acd3dfe1ce9715b657e28d138578bc40126760efb962322c56e9ca344b", size = 246284, upload-time = "2025-09-21T20:03:42.355Z" }, + { url = "https://files.pythonhosted.org/packages/db/e5/5e957ca747d43dbe4d9714358375c7546cb3cb533007b6813fc20fce37ad/coverage-7.10.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:cce2109b6219f22ece99db7644b9622f54a4e915dad65660ec435e89a3ea7cc3", size = 244353, upload-time = "2025-09-21T20:03:44.218Z" }, + { url = "https://files.pythonhosted.org/packages/9a/45/540fc5cc92536a1b783b7ef99450bd55a4b3af234aae35a18a339973ce30/coverage-7.10.7-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:f3c887f96407cea3916294046fc7dab611c2552beadbed4ea901cbc6a40cc7a0", size = 244430, upload-time = "2025-09-21T20:03:46.065Z" }, + { url = "https://files.pythonhosted.org/packages/75/0b/8287b2e5b38c8fe15d7e3398849bb58d382aedc0864ea0fa1820e8630491/coverage-7.10.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:635adb9a4507c9fd2ed65f39693fa31c9a3ee3a8e6dc64df033e8fdf52a7003f", size = 245311, upload-time = "2025-09-21T20:03:48.19Z" }, + { url = "https://files.pythonhosted.org/packages/0c/1d/29724999984740f0c86d03e6420b942439bf5bd7f54d4382cae386a9d1e9/coverage-7.10.7-cp39-cp39-win32.whl", hash = "sha256:5a02d5a850e2979b0a014c412573953995174743a3f7fa4ea5a6e9a3c5617431", size = 220500, upload-time = "2025-09-21T20:03:50.024Z" }, + { url = "https://files.pythonhosted.org/packages/43/11/4b1e6b129943f905ca54c339f343877b55b365ae2558806c1be4f7476ed5/coverage-7.10.7-cp39-cp39-win_amd64.whl", hash = "sha256:c134869d5ffe34547d14e174c866fd8fe2254918cc0a95e99052903bc1543e07", size = 221408, upload-time = "2025-09-21T20:03:51.803Z" }, + { url = "https://files.pythonhosted.org/packages/ec/16/114df1c291c22cac3b0c127a73e0af5c12ed7bbb6558d310429a0ae24023/coverage-7.10.7-py3-none-any.whl", hash = "sha256:f7941f6f2fe6dd6807a1208737b8a0cbcf1cc6d7b07d24998ad2d63590868260", size = 209952, upload-time = "2025-09-21T20:03:53.918Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version >= '3.9' and python_full_version <= '3.11'" }, +] + [[package]] name = "exceptiongroup" version = "1.3.0" @@ -161,6 +375,39 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload-time = "2025-06-18T05:48:03.955Z" }, ] +[[package]] +name = "pytest-cov" +version = "5.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +dependencies = [ + { name = "coverage", version = "7.6.1", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version < '3.9'" }, + { name = "pytest", version = "8.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/67/00efc8d11b630c56f15f4ad9c7f9223f1e5ec275aaae3fa9118c6a223ad2/pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857", size = 63042, upload-time = "2024-03-24T20:16:34.856Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/3a/af5b4fa5961d9a1e6237b530eb87dd04aea6eb83da09d2a4073d81b54ccf/pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652", size = 21990, upload-time = "2024-03-24T20:16:32.444Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.9'", +] +dependencies = [ + { name = "coverage", version = "7.10.7", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version >= '3.9'" }, + { name = "pluggy", version = "1.6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "pytest", version = "8.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, +] + [[package]] name = "tomli" version = "2.2.1"