diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index f5e0c07e6..ddf61b7e2 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -20,34 +20,70 @@ on: - '.github/workflows/lint.yml' - '.github/workflows/tests.yml' jobs: + matrix: + name: Pick Python versions + runs-on: ubuntu-24.04 + outputs: + config: ${{ steps.pick.outputs.config }} + steps: + - name: Pick + id: pick + shell: python + run: | + import json, os, random + + configs = [ + {"os": "ubuntu-24.04", "ffmpeg": "9.0.2", "extras": True}, + {"os": "ubuntu-24.04", "ffmpeg": "8.1.3"}, + {"os": "ubuntu-24.04", "ffmpeg": "9.0.2"}, + # riscv64 wheels (e.g. Pillow) are often missing on PyPI; RISE + # mirrors prebuilt wheels for it so uv doesn't have to build them + # from source. It also uses a pyav-ffmpeg prebuilt to speed up + # smoke tests. Neither index has cp315 riscv64 wheels yet. + { + "os": "ubuntu-24.04-riscv", + "ffmpeg": "9.0.2", + "prebuilt": True, + "uv-index": "https://pypi.riseproject.dev/simple/", + "pythons": ["3.12", "3.13", "3.14"], + }, + {"os": "macos-15", "ffmpeg": "8.1.3"}, + {"os": "macos-15", "ffmpeg": "9.0.2"}, + ] + pythons = ["3.12", "3.13", "3.14", "3.15"] + # Don't give two jobs with the same OS and FFmpeg the same Python. + taken = set() + for config in configs: + key = (config["os"], config["ffmpeg"]) + choices = [ + py for py in config.pop("pythons", pythons) if (key, py) not in taken + ] + config["python"] = random.choice(choices) + taken.add((key, config["python"])) + + with open(os.environ["GITHUB_OUTPUT"], "a") as f: + f.write(f"config={json.dumps(configs)}\n") + nix: - name: "py-${{ matrix.config.python }} lib-${{ matrix.config.ffmpeg }} ${{matrix.config.os}}" + name: "py${{ matrix.config.python }} lib-${{ matrix.config.ffmpeg }} ${{ matrix.config.os }}" + needs: matrix runs-on: ${{ matrix.config.os }} strategy: fail-fast: false matrix: - config: - - {os: ubuntu-24.04, python: "3.14", ffmpeg: "9.0.2", extras: true} - - {os: ubuntu-24.04, python: "3.12", ffmpeg: "8.1.2"} - - {os: ubuntu-24.04, python: "3.13", ffmpeg: "9.0.2"} - - {os: ubuntu-24.04-riscv, python: "3.14", ffmpeg: "9.0.2"} - - {os: macos-15, python: "3.12", ffmpeg: "8.1.2"} - - {os: macos-15, python: "3.14", ffmpeg: "9.0.2"} + config: ${{ fromJSON(needs.matrix.outputs.config) }} env: PYAV_PYTHON: python${{ matrix.config.python }} PYAV_LIBRARY: ffmpeg-${{ matrix.config.ffmpeg }} - # riscv64 wheels (e.g. Pillow) are often missing on PyPI; RISE mirrors - # prebuilt wheels for it so uv doesn't have to build them from source. # unsafe-best-match is needed so uv still falls back to PyPI for - # packages/versions RISE doesn't happen to carry. - UV_INDEX: ${{ matrix.config.os == 'ubuntu-24.04-riscv' && 'https://pypi.riseproject.dev/simple/' || '' }} - UV_INDEX_STRATEGY: ${{ matrix.config.os == 'ubuntu-24.04-riscv' && 'unsafe-best-match' || 'first-index' }} - # riscv64 uses a pyav-ffmpeg prebuilt to speed up smoke tests; its .pc - # files bake in prefix=/tmp/vendor, so it must be extracted there - # verbatim and pointed at explicitly. - PKG_CONFIG_PATH: ${{ matrix.config.os == 'ubuntu-24.04-riscv' && '/tmp/vendor/lib/pkgconfig' || '' }} - LD_LIBRARY_PATH: ${{ matrix.config.os == 'ubuntu-24.04-riscv' && '/tmp/vendor/lib' || '' }} + # packages/versions an extra index doesn't happen to carry. + UV_INDEX: ${{ matrix.config.uv-index || '' }} + UV_INDEX_STRATEGY: ${{ matrix.config.uv-index && 'unsafe-best-match' || 'first-index' }} + # pyav-ffmpeg prebuilt .pc files bake in prefix=/tmp/vendor, so it must + # be extracted there verbatim and pointed at explicitly. + PKG_CONFIG_PATH: ${{ matrix.config.prebuilt && '/tmp/vendor/lib/pkgconfig' || '' }} + LD_LIBRARY_PATH: ${{ matrix.config.prebuilt && '/tmp/vendor/lib' || '' }} steps: - uses: actions/checkout@v7 @@ -61,35 +97,22 @@ jobs: - name: OS Packages run: | - case ${{ matrix.config.os }} in - ubuntu-24.04) - sudo apt-get update - sudo apt-get install -y \ - autoconf \ - automake \ - build-essential \ - cmake \ - libtool \ - libx264-dev \ - nasm \ - pkg-config \ - zlib1g-dev + case ${{ runner.os }} in + Linux) + packages="build-essential pkg-config zlib1g-dev" + if [[ "${{ matrix.config.prebuilt }}" ]]; then + # libxcb-shape0 is needed for the prebuilt's X11 usage. + packages="$packages libxcb-shape0" + else + packages="$packages autoconf automake cmake libtool libx264-dev nasm" + fi if [[ "${{ matrix.config.extras }}" ]]; then - sudo apt-get install -y doxygen + packages="$packages doxygen" fi - ;; - ubuntu-24.04-riscv) - # Since we're using pyav-ffmpeg prebuilds, we don't need the full set - # of packages installed on other runners. libxcb-shape0 is needed - # for the prebuilt's X11 usage. sudo apt-get update - sudo apt-get install -y \ - build-essential \ - pkg-config \ - zlib1g-dev \ - libxcb-shape0 + sudo apt-get install -y $packages ;; - macos-15) + macOS) brew install automake libtool opus x264 ;; esac @@ -97,8 +120,7 @@ jobs: - name: Pip and FFmpeg run: | . scripts/activate.sh ffmpeg-${{ matrix.config.ffmpeg }} - # don't build from source for riscv64 - if [ "${{ matrix.config.os }}" = "ubuntu-24.04-riscv" ]; then + if [[ "${{ matrix.config.prebuilt }}" ]]; then python scripts/fetch-vendor.py --config-file scripts/ffmpeg-latest.json /tmp/vendor else scripts/build-deps diff --git a/Makefile b/Makefile index da372a93b..d20b1c902 100644 --- a/Makefile +++ b/Makefile @@ -28,7 +28,7 @@ fate-suite: rsync -vrltLW rsync://fate-suite.ffmpeg.org/fate-suite/ tests/assets/fate-suite/ lint: - $(PIP) install --only-binary pillow --group lint + $(PIP) install --only-binary numpy,pillow --group lint ruff format --check av examples tests setup.py isort --check-only --diff av examples tests mypy av tests @@ -40,5 +40,5 @@ pxdpad: pxdpad-build tools/pxdpad/bin/pxdpad av include test: - $(PIP) install --only-binary pillow --group test + $(PIP) install --only-binary numpy,pillow --group test $(PYTHON) -m pytest diff --git a/tests/test_doctests.py b/tests/test_doctests.py index 736d27c62..c336b8b64 100644 --- a/tests/test_doctests.py +++ b/tests/test_doctests.py @@ -2,6 +2,7 @@ import pkgutil import re import types +import unittest from unittest import TestCase import av @@ -32,7 +33,14 @@ def register_doctests(mod: str | types.ModuleType) -> None: assert isinstance(test, doctest.DocTestCase) def func(self: TestCase, test: doctest.DocTestCase = test) -> None: - return test.runTest() + # Go through run(); since Python 3.15, runTest() reports to the + # result that only run() sets. + result = unittest.TestResult() + test.run(result) + if problems := result.errors + result.failures: + self.fail("\n".join(tb for _, tb in problems)) + if any(t is test for t, _ in result.skipped): + self.skipTest("all examples were skipped") name = str("test_" + re.sub("[^a-zA-Z0-9]+", "_", test.id()).strip("_")) func.__name__ = name