From 46022cba527ec912f0a00caba1f8ed5f4444a9e0 Mon Sep 17 00:00:00 2001 From: Matt Hammerly Date: Wed, 28 Aug 2024 10:49:43 -0700 Subject: [PATCH] initialize maturin package --- .github/workflows/ci.yml | 63 +++++++++++++++-- .gitignore | 8 +++ .pre-commit-config.yaml | 21 +++++- Cargo.lock | 116 ++++++++++++++++++++++++++++++++ Cargo.toml | 3 +- Makefile | 16 +++++ README.md | 17 +++-- bindings/Cargo.toml | 17 +++++ bindings/src/lib.rs | 18 +++++ codecov.yml | 33 +++++++++ pyproject.toml | 71 +++++++++++++++++++ python/codecov_rs/__init__.py | 0 python/codecov_rs/dummy_add.py | 3 + python/codecov_rs/dummy_add.pyi | 3 + python/codecov_rs/py.typed | 0 python/requirements.dev.txt | 6 ++ python/tests/test_lib.py | 4 ++ 17 files changed, 387 insertions(+), 12 deletions(-) create mode 100644 Makefile create mode 100644 bindings/Cargo.toml create mode 100644 bindings/src/lib.rs create mode 100644 codecov.yml create mode 100644 pyproject.toml create mode 100644 python/codecov_rs/__init__.py create mode 100644 python/codecov_rs/dummy_add.py create mode 100644 python/codecov_rs/dummy_add.pyi create mode 100644 python/codecov_rs/py.typed create mode 100644 python/requirements.dev.txt create mode 100644 python/tests/test_lib.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d3f38e2..48bf020 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,9 +24,22 @@ jobs: toolchain: nightly components: clippy, rustfmt - name: Run lint + run: make lint.rust + + lint-python: + name: Lint Python (ruff, mypy) + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - uses: actions/setup-python@v3 + with: + python-version: '3.12' + - name: Build and run lint run: | - cargo fmt --all --check - cargo clippy + make ci.setup_venv + maturin develop + make lint.python build: name: Build @@ -43,6 +56,14 @@ jobs: - name: Run build run: | cargo build + - uses: actions/setup-python@v3 + with: + python-version: '3.12' + - name: Install Python requirements + run: pip install -r python/requirements.dev.txt + - name: Build Python bindings + run: maturin build + test: name: Test runs-on: ubuntu-latest @@ -55,9 +76,19 @@ jobs: uses: dtolnay/rust-toolchain@nightly with: toolchain: nightly - - name: Run tests + - name: Run Rust tests run: | cargo test + + - uses: actions/setup-python@v3 + with: + python-version: '3.12' + - name: Run Python tests + run: | + make ci.setup_venv + maturin develop + pytest + # This job runs tests, generates coverage data, and generates JUnit test # results in a single test invocation and then uploads it all to Codecov. # However, it doesn't print test results to stdout. If Codecov's failed test @@ -80,16 +111,38 @@ jobs: - name: Run tests run: | cargo install cargo2junit - cargo llvm-cov --lcov --output-path lcov.info -- -Z unstable-options --format json --report-time | cargo2junit > results.xml + cargo llvm-cov --lcov --output-path core.lcov -- -Z unstable-options --format json --report-time | cargo2junit > core-test-results.xml + + - uses: actions/setup-python@v3 + with: + python-version: '3.12' + - name: Run Python tests + run: | + make ci.setup_venv + + # Clear prior profile data + cargo llvm-cov clean --workspace + + # Set env vars so maturin will build our Rust code with coverage instrumentation + source <(cargo llvm-cov show-env --export-prefix) + maturin develop + + # Run Python tests. Any Rust code exercised by these tests will emit coverage data + pytest --cov --junitxml=python-test-results.xml + + # Turn the Rust coverage data into an lcov file + cargo llvm-cov --no-run --lcov --output-path bindings.lcov + - name: Upload coverage data to Codecov if: ${{ !cancelled() }} uses: codecov/codecov-action@v4 with: + files: ./core.lcov,./bindings.lcov,./.coverage token: ${{ secrets.CODECOV_ORG_TOKEN }} - name: Upload test results to Codecov if: ${{ !cancelled() }} uses: codecov/test-results-action@v1 with: - file: ./results.xml + files: ./core-test-results.xml,./python-test-results.xml token: ${{ secrets.CODECOV_ORG_TOKEN }} verbose: true diff --git a/.gitignore b/.gitignore index 55b9ef7..6e0571f 100644 --- a/.gitignore +++ b/.gitignore @@ -17,5 +17,13 @@ target/ # Vim swapfiles .*.sw* +# Python junk +__pycache__/ +*.py[cod] +*$py.class + +# Native extensions for Python +*.so + .git lcov.info diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 536e92a..34958ca 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,7 @@ repos: - repo: local hooks: - - id: format + - id: rustfmt name: cargo fmt entry: cargo fmt args: ["--check", "--"] @@ -16,3 +16,22 @@ repos: pass_filenames: false types: [rust] language: system + - id: ruffcheck + name: ruff check + entry: ruff check + require_serial: true + types: [python] + language: system + - id: rufffmt + name: ruff format + entry: ruff format + require_serial: true + types: [python] + language: system + - repo: https://github.com/pre-commit/mirrors-mypy + rev: 'v1.10.0' + hooks: + - id: mypy + verbose: true + entry: bash -c 'mypy "$@" || true' -- + types: [python] diff --git a/Cargo.lock b/Cargo.lock index 9f05a2c..3742f4b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20,6 +20,12 @@ version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + [[package]] name = "bitflags" version = "2.6.0" @@ -87,6 +93,14 @@ dependencies = [ "winnow", ] +[[package]] +name = "codecov-rs-bindings" +version = "0.1.0" +dependencies = [ + "codecov-rs", + "pyo3", +] + [[package]] name = "condtype" version = "1.3.0" @@ -175,6 +189,12 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "include_dir" version = "0.7.4" @@ -194,6 +214,12 @@ dependencies = [ "quote", ] +[[package]] +name = "indoc" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" + [[package]] name = "itoa" version = "1.0.11" @@ -244,6 +270,15 @@ dependencies = [ "libc", ] +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + [[package]] name = "once_cell" version = "1.19.0" @@ -256,6 +291,12 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" +[[package]] +name = "portable-atomic" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da544ee218f0d287a911e9c99a39a8c9bc8fcad3cb8db5959940044ecfc67265" + [[package]] name = "ppv-lite86" version = "0.2.20" @@ -274,6 +315,69 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "pyo3" +version = "0.22.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831e8e819a138c36e212f3af3fd9eeffed6bf1510a805af35b0edee5ffa59433" +dependencies = [ + "cfg-if", + "indoc", + "libc", + "memoffset", + "once_cell", + "portable-atomic", + "pyo3-build-config", + "pyo3-ffi", + "pyo3-macros", + "unindent", +] + +[[package]] +name = "pyo3-build-config" +version = "0.22.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e8730e591b14492a8945cdff32f089250b05f5accecf74aeddf9e8272ce1fa8" +dependencies = [ + "once_cell", + "target-lexicon", +] + +[[package]] +name = "pyo3-ffi" +version = "0.22.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e97e919d2df92eb88ca80a037969f44e5e70356559654962cbb3316d00300c6" +dependencies = [ + "libc", + "pyo3-build-config", +] + +[[package]] +name = "pyo3-macros" +version = "0.22.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb57983022ad41f9e683a599f2fd13c3664d7063a3ac5714cae4b7bee7d3f206" +dependencies = [ + "proc-macro2", + "pyo3-macros-backend", + "quote", + "syn", +] + +[[package]] +name = "pyo3-macros-backend" +version = "0.22.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec480c0c51ddec81019531705acac51bcdbeae563557c982aa8263bb96880372" +dependencies = [ + "heck", + "proc-macro2", + "pyo3-build-config", + "quote", + "syn", +] + [[package]] name = "quote" version = "1.0.36" @@ -418,6 +522,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + [[package]] name = "tempfile" version = "3.12.0" @@ -467,6 +577,12 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +[[package]] +name = "unindent" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7de7d73e1754487cb58364ee906a499937a0dfabd86bcb980fa99ec8c8fa2ce" + [[package]] name = "vcpkg" version = "0.2.15" diff --git a/Cargo.toml b/Cargo.toml index 59a619c..b29b84d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,8 @@ [workspace] resolver = "2" -members = ["core"] +members = ["bindings", "core"] +default-members = ["core"] [profile.release] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..1263022 --- /dev/null +++ b/Makefile @@ -0,0 +1,16 @@ +ci.setup_venv: + python3 -m venv .venv + source .venv/bin/activate + # Make future steps also use the venv + echo PATH=$PATH >> $GITHUB_ENV + pip install -r python/requirements.dev.txt + + +lint.rust: + cargo fmt --all --check + cargo clippy + +lint.python: + ruff check + ruff format + mypy diff --git a/README.md b/README.md index ae7701d..e16e4f0 100644 --- a/README.md +++ b/README.md @@ -16,26 +16,29 @@ All details (e.g. SQLite schema, code interfaces) subject to breaking changes un ## Developing -At time of writing, `codecov-rs` requires the nightly compiler for niceties such as `#[feature(trait_alias)]` in the library itself. +Set up your development environment: +- Install the nightly compiler via [rustup](https://rustup.rs/). At time of writing, `codecov-rs` requires the nightly compiler for niceties such as `#[feature(trait_alias)]`. +- To work on the Python bindings, set up a virtualenv with your tool of choice and install our Python dependencies: `pip install -r python/requirements.dev.txt`. +- Install lint hooks with `pip install pre-commit && pre-commit install`. +- Large sample test reports are checked in using [Git LFS](https://git-lfs.com/) in `core/fixtures/**/large` directories (e.g. `core/fixtures/pyreport/large`). Tests and benchmarks may reference them so installing it yourself is recommended. `codecov-rs` aims to serve as effective documentation for every flavor of every format it supports. To that end, the following are greatly appreciated in submissions: - Thorough doc comments (`///` / `/**`). For parsers, include snippets that show what inputs look like - Granular, in-module unit tests - Integration tests with real-world samples (that are safe to distribute; don't send us data from your private repo) -Large sample test reports are checked in using [Git LFS](https://git-lfs.com/) in `core/fixtures/**/large` directories (e.g. `core/fixtures/pyreport/large`). Tests and benchmarks may reference them so installing it yourself is recommended. - The `examples/` directory contains runnable commands for developers including: - `parse_pyreport`: converts a given pyreport into a SQLite report - `sql_to_pyreport`: converts a given SQLite report into a pyreport (report JSON + chunks file) You can run an example with `cargo run --example `. Consider following suit for your own new feature. -Install lint hooks with `pip install pre-commit && pre-commit install`. - ### Repository structure - `core/`: Rust crate with all of the core coverage-processing functionality +- `bindings/`: Rust crate with PyO3 bindings for `core/` +- `python/codecov_rs`: Python code using/typing the Rust crate in `bindings/` +- `python/tests`: Python tests ### Writing new parsers @@ -58,7 +61,11 @@ Non-XML formats lack clean OOTB support for streaming so `codecov-rs` currently Run tests with: ``` +# Rust tests $ cargo test + +# Python tests +$ pytest ``` ### Benchmarks diff --git a/bindings/Cargo.toml b/bindings/Cargo.toml new file mode 100644 index 0000000..987ca58 --- /dev/null +++ b/bindings/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "codecov-rs-bindings" +version = "0.1.0" +publish = false +edition = "2021" + +[lib] +name = "_bindings" +crate-type = ["cdylib"] + +[dependencies] +codecov-rs = { path = "../core" } + +pyo3 = { version = "0.22.2", features = [ + "extension-module", + "abi3-py311", +] } diff --git a/bindings/src/lib.rs b/bindings/src/lib.rs new file mode 100644 index 0000000..696a20b --- /dev/null +++ b/bindings/src/lib.rs @@ -0,0 +1,18 @@ +use pyo3::prelude::*; + +// See if non-pyo3-annotated Rust lines are still instrumented +fn raw_rust_add(a: usize, b: usize) -> usize { + println!("hello"); + a + b +} + +#[pyfunction] +fn dummy_add(a: usize, b: usize) -> PyResult { + Ok(raw_rust_add(a, b)) +} + +#[pymodule] +fn _bindings(_py: Python, m: &Bound) -> PyResult<()> { + m.add_function(wrap_pyfunction!(dummy_add, m)?)?; + Ok(()) +} diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..f67f207 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,33 @@ +comment: + layout: "condensed_header, diff, condensed_files, components, condensed_footer" + +coverage: + status: + project: + default: + target: auto + threshold: 1 # 1% drops are frowny, 0.1% drops are fine + patch: + default: + target: auto + threshold: 1 # 1% drops are frowny, 0.1% drops are fine + +component_management: + default_rules: + statuses: + # No need for per-component statuses for now + individual_components: + - component_id: rust_core + name: core + paths: + - core/** + + - component_id: rust_bindings + name: bindings + paths: + - bindings/** + + - component_id: python_package + name: python + paths: + - python/codecov_rs/** diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..7cd176f --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,71 @@ +[build-system] +requires = ["maturin>=1.3,<2.0"] +build-backend = "maturin" + +[project] +name = "codecov_rs" +authors = [ + {name = "Codecov", email = "support@codecov.io"}, +] +readme = "README.md" +requires-python = ">=3.11" +license = { file = "LICENSE.md" } +classifiers = [ + "Programming Language :: Rust", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", +] +dependencies = [] +dynamic = ["version"] + +[tool.maturin] +module-name = "codecov_rs._bindings" +manifest-path = "bindings/Cargo.toml" +python-source = "python" + + +[tool.mypy] +python_version = "3.11" +plugins = ["pydantic.mypy"] + +# Don't bother scanning Rust directories +files = ["python/"] + +[tool.ruff] +# Don't bother scanning Rust directories +include = ["pyproject.toml", "python/**/*.py", "python/**/*.pyi"] + +# Same as Black. +line-length = 88 +indent-width = 4 + +# Assume Python 3.11 +target-version = "py311" + +[tool.ruff.lint] +# Currently only enabled for F (Pyflakes), I (isort), E,W (pycodestyle:Error/Warning), PLC/PLE (Pylint:Convention/Error) +# and PERF (Perflint) rules: https://docs.astral.sh/ruff/rules/ +select = ["F", "I", "E", "W", "PLC", "PLE", "PERF"] + +# Allow fix for all enabled rules (when `--fix`) is provided. +fixable = ["ALL"] +unfixable = [] + +# Allow unused variables when underscore-prefixed. +dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" + +[tool.ruff.format] +# Like Black, use double quotes for strings. +quote-style = "double" + +# Like Black, indent with spaces, rather than tabs. +indent-style = "space" + +# Like Black, respect magic trailing commas. +skip-magic-trailing-comma = false + +# Like Black, automatically detect the appropriate line ending. +line-ending = "auto" + +[tool.pytest.ini_options] +testpaths = ["python/tests"] diff --git a/python/codecov_rs/__init__.py b/python/codecov_rs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python/codecov_rs/dummy_add.py b/python/codecov_rs/dummy_add.py new file mode 100644 index 0000000..6801173 --- /dev/null +++ b/python/codecov_rs/dummy_add.py @@ -0,0 +1,3 @@ +from ._bindings import dummy_add + +dummy_add.__module__ = __name__ diff --git a/python/codecov_rs/dummy_add.pyi b/python/codecov_rs/dummy_add.pyi new file mode 100644 index 0000000..8d003c5 --- /dev/null +++ b/python/codecov_rs/dummy_add.pyi @@ -0,0 +1,3 @@ +from pydantic.types import NonNegativeInt + +def dummy_add(a: NonNegativeInt, b: NonNegativeInt) -> NonNegativeInt: ... diff --git a/python/codecov_rs/py.typed b/python/codecov_rs/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/python/requirements.dev.txt b/python/requirements.dev.txt new file mode 100644 index 0000000..704f0a4 --- /dev/null +++ b/python/requirements.dev.txt @@ -0,0 +1,6 @@ +maturin==1.7.1 +mypy==1.11.2 +pydantic==2.8.2 +pytest==8.3.2 +pytest-cov==5.0.0 +ruff==0.6.2 diff --git a/python/tests/test_lib.py b/python/tests/test_lib.py new file mode 100644 index 0000000..ec3a03f --- /dev/null +++ b/python/tests/test_lib.py @@ -0,0 +1,4 @@ +def test_dummy_add(): + from codecov_rs.dummy_add import dummy_add + + assert dummy_add(3, 4) == 7