diff --git a/.github/workflows/install-linux-test-deps.sh b/.github/workflows/install-linux-test-deps.sh new file mode 100644 index 00000000..8a2ebef6 --- /dev/null +++ b/.github/workflows/install-linux-test-deps.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +set -euo pipefail + +# cryptography has no CPython 3.15t wheels yet. These dependencies are only +# needed to build test dependencies inside the cibuildwheel container. +if command -v apk >/dev/null 2>&1; then + apk add --no-cache openssl-dev libffi-dev pkgconf +else + # manylinux_2_28 ships OpenSSL 1.1.1; cryptography needs OpenSSL 3. + yum install -y openssl3-devel libffi-devel pkgconfig + # EPEL names its pkg-config file openssl3.pc; the build looks for openssl. + mkdir -p /usr/local/lib/pkgconfig + ln -s /usr/lib64/pkgconfig/openssl3.pc /usr/local/lib/pkgconfig/openssl.pc +fi diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b194aa42..f0e52471 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -75,7 +75,11 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, macos-latest, ubuntu-22.04-arm] + os: + - ubuntu-latest + - ubuntu-22.04-arm + - macos-15-intel + - macos-latest python: - "cp38" - "cp39" @@ -85,17 +89,17 @@ jobs: - "cp313" - "cp314" - "cp314t" - cibw_arch: ["x86_64", "aarch64", "universal2"] - exclude: + - "cp315" + - "cp315t" + include: - os: ubuntu-latest - cibw_arch: universal2 - - os: ubuntu-latest - cibw_arch: aarch64 - - os: macos-latest - cibw_arch: aarch64 - - os: ubuntu-22.04-arm cibw_arch: x86_64 - os: ubuntu-22.04-arm + cibw_arch: aarch64 + # Use native OpenSSL when compiling x86_64 test dependencies. + - os: macos-15-intel + cibw_arch: x86_64 + - os: macos-latest cibw_arch: universal2 defaults: @@ -104,6 +108,13 @@ jobs: env: PIP_DISABLE_PIP_VERSION_CHECK: 1 + CIBW_BUILD_VERBOSITY: 1 + CIBW_BUILD: ${{ matrix.python }}-* + CIBW_ARCHS: ${{ matrix.cibw_arch }} + # Let tests inside Linux containers recognize GitHub CI. + CIBW_ENVIRONMENT_PASS_LINUX: GITHUB_WORKFLOW + # Exercise arm64 here; x86_64 wheels have their own test jobs. + CIBW_TEST_SKIP: "*universal2:x86_64" steps: - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 @@ -114,14 +125,23 @@ jobs: - name: Install macOS deps if: startsWith(matrix.os, 'macos') run: | - brew install gnu-sed libtool autoconf automake + brew install gnu-sed libtool autoconf automake openssl@3 + echo "OPENSSL_DIR=$(brew --prefix openssl@3)" >> "$GITHUB_ENV" + # cibuildwheel 4 added Python 3.15 but removed Python 3.8. - uses: pypa/cibuildwheel@7c619efba910c04005a835b110b057fc28fd6e93 # v3.2.0 + if: matrix.python == 'cp38' + + - uses: pypa/cibuildwheel@1828c10ab37f080699c7b81cea34097c684a7074 # v4.2.0 + if: matrix.python != 'cp38' env: - CIBW_BUILD_VERBOSITY: 1 - CIBW_BUILD: ${{ matrix.python }}-* - CIBW_ARCHS: ${{ matrix.cibw_arch }} - CIBW_TEST_SKIP: "*universal2:arm64" + CIBW_BEFORE_ALL_LINUX: >- + ${{ matrix.python == 'cp315t' && 'bash {project}/.github/workflows/install-linux-test-deps.sh' || '' }} + # Build universal2 wheels, but compile their test dependencies for arm64. + CIBW_TEST_ENVIRONMENT_MACOS: >- + ${{ matrix.cibw_arch == 'universal2' + && 'ARCHFLAGS="-arch arm64" _PYTHON_HOST_PLATFORM="macosx-11.0-arm64"' + || '' }} - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ec325e5f..2fd086db 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -24,6 +24,8 @@ jobs: - "3.13" - "3.14" - "3.14t" + - "3.15" + - "3.15t" os: [ubuntu-latest, macos-latest] env: @@ -55,7 +57,8 @@ jobs: - name: Install macOS deps if: matrix.os == 'macos-latest' && steps.release.outputs.version == 0 run: | - brew install gnu-sed libtool autoconf automake + brew install gnu-sed libtool autoconf automake openssl@3 + echo "OPENSSL_DIR=$(brew --prefix openssl@3)" >> "$GITHUB_ENV" - name: Install Python Deps if: steps.release.outputs.version == 0 diff --git a/pyproject.toml b/pyproject.toml index 1802b406..d547ccae 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,7 +41,9 @@ test = [ 'flake8~=6.1', 'psutil', 'pycodestyle~=2.11.0', - 'pyOpenSSL~=25.3.0', + # pyOpenSSL 26.4 requires Python 3.9 or later. + 'pyOpenSSL~=25.3.0; python_version < "3.9"', + 'pyOpenSSL~=26.4.0; python_version >= "3.9"', 'mypy>=0.800', ] dev = [ diff --git a/tests/test_base.py b/tests/test_base.py index 5506748d..1eaa1b93 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -290,9 +290,16 @@ def check_in_thread(loop, event, debug, create_loop, fut): def test_thread(loop, debug, create_loop=False): event = threading.Event() fut = asyncio.Future(loop=loop) - loop.call_soon(event.set) args = (loop, event, debug, create_loop, fut) thread = threading.Thread(target=check_in_thread, args=args) + + def run_thread(): + # Keep the loop running for the thread checks, but prevent + # concurrent access to its non-thread-safe scheduling APIs. + event.set() + thread.join() + + loop.call_soon(run_thread) thread.start() loop.run_until_complete(fut) thread.join() diff --git a/tests/test_process.py b/tests/test_process.py index 45036256..7b06e76d 100644 --- a/tests/test_process.py +++ b/tests/test_process.py @@ -884,9 +884,10 @@ class TestAsyncio_AIO_Process(_AsyncioTests, tb.AIOTestCase): class Test_UV_Process_Delayed(tb.UVTestCase): class TestProto: - def __init__(self): + def __init__(self, closed): self.lost = 0 self.stages = [] + self.closed = closed def connection_made(self, transport): self.stages.append(('CM', transport)) @@ -905,10 +906,11 @@ def process_exited(self): def connection_lost(self, exc): self.stages.append(('CL', self.lost, exc)) self.lost += 1 + self.closed.set_result(None) async def run_sub(self, **kwargs): return await self.loop.subprocess_shell( - lambda: self.TestProto(), + lambda: self.TestProto(self.loop.create_future()), 'echo 1', **kwargs) @@ -962,7 +964,8 @@ def test_process_delayed_stdio__not_paused__no_stdin(self): stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE)) - self.loop.run_until_complete(transport._wait()) + # Process exit alone does not guarantee that pipe callbacks have run. + self.loop.run_until_complete(asyncio.wait_for(proto.closed, 10)) self.assertEqual(transport.get_returncode(), 0) self.assertIsNot(transport, None) self.assertEqual( diff --git a/tests/test_sockets.py b/tests/test_sockets.py index e7c335e1..63cc8a6d 100644 --- a/tests/test_sockets.py +++ b/tests/test_sockets.py @@ -1,4 +1,5 @@ import asyncio +import os import pickle import select import socket @@ -272,7 +273,9 @@ def test_socket_sync_remove(self): # See https://github.com/MagicStack/uvloop/issues/61 for details sock = socket.socket() - epoll = select.epoll.fromfd(self.loop._get_backend_id()) + # Keep a separate descriptor for the backend epoll instance. The loop + # owns its descriptor and closes it when the loop is closed. + epoll = select.epoll.fromfd(os.dup(self.loop._get_backend_id())) try: cb = lambda: None @@ -288,8 +291,8 @@ def test_socket_sync_remove(self): finally: sock.close() - self.loop.close() epoll.close() + self.loop.close() def test_add_reader_or_writer_transport_fd(self): def assert_raises():