diff --git a/.coveragerc-cython.toml b/.coveragerc-cython.toml new file mode 100644 index 00000000000..586dc937786 --- /dev/null +++ b/.coveragerc-cython.toml @@ -0,0 +1,17 @@ +[run] +branch = true +plugins = [ + 'Cython.Coverage', +] +omit = [ + 'site-packages', +] + +[report] +exclude_also = [ + 'if TYPE_CHECKING', + 'assert False', + ': \.\.\.(\s*#.*)?$', + '^ +\.\.\.$', + 'pytest.fail\(' +] diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 1eed9f30924..ddc3b283094 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -416,6 +416,64 @@ jobs: run: python -Im pytest --no-cov --numprocesses=0 -vvvvv --codspeed + cython-coverage: + permissions: + contents: read # to fetch code (actions/checkout) + + name: Cython coverage + needs: gen_llhttp + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + submodules: true + - name: Setup Python + id: python-install + uses: actions/setup-python@v6 + with: + python-version: '3.12' + - name: Update pip, wheel, setuptools, build, twine + run: | + python -m pip install -U pip wheel setuptools build twine + - name: Install dependencies + run: | + python -Im pip install -r requirements/test.in -c requirements/test.txt + - name: Uninstall blocbuster + run: python -m pip uninstall blockbuster -y + - name: Restore llhttp generated files + uses: actions/download-artifact@v8 + with: + name: llhttp + path: vendor/llhttp/build/ + - name: Cythonize with linetrace + run: | + make cythonize CYTHON_EXTRA="-X linetrace=True" + - name: Install self + env: + AIOHTTP_CYTHON_TRACE: 1 + run: python -m pip install -e . + - name: Run tests with Cython tracing + env: + COLOR: yes + PIP_USER: 1 + run: >- + pytest tests/test_client_functional.py tests/test_http_parser.py tests/test_http_writer.py tests/test_web_functional.py tests/test_web_response.py tests/test_websocket_parser.py + --cov-config=.coveragerc-cython.toml + -m 'not dev_mode and not autobahn' + shell: bash + - name: Turn coverage into xml + run: | + python -m coverage xml -o cython-coverage.xml --rcfile=.coveragerc-cython.toml + - name: Upload coverage + uses: codecov/codecov-action@v6 + with: + files: ./cython-coverage.xml + disable_search: true + flags: cython-coverage + token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: true + check: # This job does nothing and is only used for the branch protection if: always() diff --git a/.github/workflows/label-remove.yml b/.github/workflows/label-remove.yml index 24e601d754a..9cbd68d823c 100644 --- a/.github/workflows/label-remove.yml +++ b/.github/workflows/label-remove.yml @@ -11,6 +11,7 @@ jobs: clear-pr-unfinished: runs-on: ubuntu-latest permissions: + id-token: write pull-requests: write steps: diff --git a/CHANGES/12349.contrib.rst b/CHANGES/12349.contrib.rst new file mode 100644 index 00000000000..12aeb069354 --- /dev/null +++ b/CHANGES/12349.contrib.rst @@ -0,0 +1 @@ +Added a CI job to measure Cython coverage -- by :user:`Dreamsorcerer`. diff --git a/Makefile b/Makefile index 7fa6aa0d437..f0003454b0c 100644 --- a/Makefile +++ b/Makefile @@ -3,6 +3,7 @@ to-hash-one = $(dir $1).hash/$(addsuffix .hash,$(notdir $1)) to-hash = $(foreach fname,$1,$(call to-hash-one,$(fname))) +CYTHON_EXTRA ?= CYS := $(wildcard aiohttp/*.pyx) $(wildcard aiohttp/*.pyi) $(wildcard aiohttp/*.pxd) $(wildcard aiohttp/_websocket/*.pyx) $(wildcard aiohttp/_websocket/*.pyi) $(wildcard aiohttp/_websocket/*.pxd) PYXS := $(wildcard aiohttp/*.pyx) $(wildcard aiohttp/_websocket/*.pyx) CS := $(wildcard aiohttp/*.c) $(wildcard aiohttp/_websocket/*.c) @@ -59,14 +60,14 @@ aiohttp/_find_header.c: $(call to-hash,aiohttp/hdrs.py ./tools/gen.py) # Special case for reader since we want to be able to disable # the extension with AIOHTTP_NO_EXTENSIONS aiohttp/_websocket/reader_c.c: aiohttp/_websocket/reader_c.py - cython -3 -X freethreading_compatible=True -o $@ $< -I aiohttp -Werror + cython -3 -X freethreading_compatible=True $(CYTHON_EXTRA) -o $@ $< -I aiohttp -Werror # _find_headers generator creates _headers.pyi as well aiohttp/%.c: aiohttp/%.pyx $(call to-hash,$(CYS)) aiohttp/_find_header.c - cython -3 -X freethreading_compatible=True -o $@ $< -I aiohttp -Werror + cython -3 -X freethreading_compatible=True $(CYTHON_EXTRA) -o $@ $< -I aiohttp -Werror aiohttp/_websocket/%.c: aiohttp/_websocket/%.pyx $(call to-hash,$(CYS)) - cython -3 -X freethreading_compatible=True -o $@ $< -I aiohttp -Werror + cython -3 -X freethreading_compatible=True $(CYTHON_EXTRA) -o $@ $< -I aiohttp -Werror vendor/llhttp/node_modules: vendor/llhttp/package.json cd vendor/llhttp; npm ci diff --git a/setup.py b/setup.py index 96cfa50d47e..72fd074b66f 100644 --- a/setup.py +++ b/setup.py @@ -12,6 +12,7 @@ os.environ.get("AIOHTTP_USE_SYSTEM_DEPS", os.environ.get("USE_SYSTEM_DEPS")) ) NO_EXTENSIONS: bool = bool(os.environ.get("AIOHTTP_NO_EXTENSIONS")) +CYTHON_TRACING: bool = bool(os.environ.get("AIOHTTP_CYTHON_TRACE")) HERE = pathlib.Path(__file__).parent IS_GIT_REPO = (HERE / ".git").exists() @@ -54,8 +55,16 @@ "include_dirs": ["vendor/llhttp/build"], } +cython_trace_macros = [("CYTHON_TRACE", 1)] if CYTHON_TRACING else [] +if cython_trace_macros: + llhttp_kwargs.setdefault("define_macros", []).extend(cython_trace_macros) + extensions = [ - Extension("aiohttp._websocket.mask", ["aiohttp/_websocket/mask.c"]), + Extension( + "aiohttp._websocket.mask", + ["aiohttp/_websocket/mask.c"], + define_macros=cython_trace_macros, + ), Extension( "aiohttp._http_parser", [ @@ -65,8 +74,16 @@ ], **llhttp_kwargs, ), - Extension("aiohttp._http_writer", ["aiohttp/_http_writer.c"]), - Extension("aiohttp._websocket.reader_c", ["aiohttp/_websocket/reader_c.c"]), + Extension( + "aiohttp._http_writer", + ["aiohttp/_http_writer.c"], + define_macros=cython_trace_macros, + ), + Extension( + "aiohttp._websocket.reader_c", + ["aiohttp/_websocket/reader_c.c"], + define_macros=cython_trace_macros, + ), ]