# pyuppsala - Python bindings for the Uppsala XML library
default:
    @just --list

# ─── Build & test ───

# Build the native extension in development mode
build:
    uv run maturin develop

# Build the native extension optimized
build-release:
    uv run maturin develop --release

# Run the Python test suite
test:
    uv run pytest tests/ -v

# Run a single test, e.g. `just test-one test_parse_simple`
test-one name:
    uv run pytest tests/ -v -k {{name}}

# Build a release wheel
wheel:
    uv run maturin build --release

# Build the Sphinx docs
docs:
    cd docs && make html

# ─── Benchmarks (pyuppsala vs lxml; lxml is a dev-group dependency) ───
#
# The aggregate defaults to the pyFF test fixture in a sibling checkout
# (../pyFF/src/pyff/test/data/metadata/swamid-2.0-test.xml). Pass another SAML
# EntitiesDescriptor file as the first argument. The extension is rebuilt in
# release mode first so the numbers never come from a stale debug build.

# Per-operation pyFF-shaped comparison against lxml (ratio column = pyuppsala/lxml)
bench-lxml aggregate="" budget="0.5" json="":
    #!/usr/bin/env bash
    set -euo pipefail
    uv run maturin develop --release >&2
    args=()
    [[ -n "{{aggregate}}" ]] && args+=("{{aggregate}}")
    [[ -n "{{json}}" ]] && args+=(--json "{{json}}")
    cmd=(uv run python benchmarks/etree_bench.py "${args[@]}" --budget "{{budget}}")
    if command -v taskset >/dev/null 2>&1; then
        taskset -c 0 "${cmd[@]}"
    else
        "${cmd[@]}"
    fi

# Compiled-XPath reuse vs per-call compilation on the aggregate
bench-xpath aggregate="":
    uv run maturin develop --release >&2
    uv run python benchmarks/xpath_pattern.py {{aggregate}}

# Batch / parallel ingest (parse_many, fetch_many); add --fetch for the HTTP rows
bench-parallel *args:
    uv run maturin develop --release >&2
    uv run python benchmarks/parallel_bench.py {{args}}

# ─── Fuzzing (top-level fuzz/, Atheris + libFuzzer over the PyO3 extension) ───
#
# OSS-Fuzz-style layout: harnesses are fuzz/<name>_fuzzer.py. Atheris drives the
# same input->crash loop as uppsala's cargo-fuzz targets, but against the Python
# API of the compiled extension. The oracle is "any exception that is NOT a
# documented malformed-input error is a finding" (Rust panics surface as
# PanicException; hangs/OOM are caught by -timeout / -rss_limit_mb; native
# faults by faulthandler, or ASan in the ASAN=1 build). See fuzz/README.md.

# Install the fuzzing toolchain (atheris + coverage). Run once per machine.
fuzz-setup:
    uv pip install -r fuzz/requirements.txt

# Build the extension for fuzzing. Retargets uppsala to git `main` via a
# [patch.crates-io] override (Cargo.toml untouched; CI keeps the crates.io dep).
# Env: UPPSALA_REF=<branch> UPPSALA_GIT=<url> UPPSALA_PATH=<local> ASAN=1
fuzz-build:
    ./fuzz/scripts/build.sh

# Fuzz ALL targets in a detached tmux session (remote-friendly).
# Arg = seconds per target, 0 = forever.
fuzz seconds="0":
    ./fuzz/scripts/fuzz-all.sh {{seconds}}

# Fuzz a single target in the foreground.
# e.g. `just fuzz-one roundtrip_fuzzer 3600`
fuzz-one target seconds="0":
    ./fuzz/scripts/run.sh {{target}} {{seconds}}

# Reattach to the running fuzz session.
fuzz-attach:
    tmux attach -t pyuppsala-fuzz

# Stop all fuzzing (kills the tmux session).
fuzz-stop:
    tmux kill-session -t pyuppsala-fuzz || true

# List any crash/timeout/oom artifacts found so far.
fuzz-crashes:
    @find fuzz/artifacts -type f 2>/dev/null | sort || echo "no artifacts yet"

# Reproduce a crash: `just fuzz-repro parse_fuzzer fuzz/artifacts/parse_fuzzer/crash-abc`
fuzz-repro target artifact:
    ./fuzz/scripts/repro.sh {{target}} {{artifact}}

# Best-effort etree-layer line-coverage report for a target's corpus.
fuzz-coverage target:
    ./fuzz/scripts/coverage.sh {{target}}

# Minimize a target's corpus (drop redundant inputs).
fuzz-cmin target:
    ./fuzz/scripts/minimize.sh {{target}}
