diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 833a048..5fa2f7c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -45,13 +45,6 @@ jobs: - uses: astral-sh/setup-uv@v7 - run: uv build --sdist - uses: actions/upload-artifact@v7 - id: upload - continue-on-error: true - with: - name: sdist - path: dist/*.tar.gz - - uses: actions/upload-artifact@v7 - if: steps.upload.outcome == 'failure' with: name: sdist path: dist/*.tar.gz @@ -65,13 +58,18 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, ubuntu-24.04-arm, macos-15-intel, macos-15, windows-latest] - # PRs: oldest and newest only, with and without the GIL - python: ${{ fromJSON(github.event_name == 'pull_request' && '["cp39","cp315","cp315t"]' || '["cp39","cp310","cp311","cp312","cp313","cp314","cp314t","cp315","cp315t"]') }} + # PRs and main: oldest and newest only, with and without the GIL; all of them for tags, Fridays and by hand + python: ${{ fromJSON((startsWith(github.ref, 'refs/tags/') || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') && '["cp39","cp310","cp311","cp312","cp313","cp314","cp314t","cp315","cp315t"]' || '["cp39","cp315","cp315t"]') }} + # Intel macOS with the full matrix only + exclude: + - os: ${{ !(startsWith(github.ref, 'refs/tags/') || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') && 'macos-15-intel' || '' }} env: WRTC_CACHE_DIR: ${{ github.workspace }}/.wrtc-cache WRTC_ARCHIVE_DIR: ${{ matrix.os == 'windows-latest' && format('{0}/.wrtc-cache/archives', github.workspace) || '' }} UV_HTTP_RETRIES: 10 CIBW_ENVIRONMENT_PASS_LINUX: UV_HTTP_RETRIES + # --gc-on-emit not on main and tags + CIBW_TEST_COMMAND: ${{ github.event_name != 'push' && 'pytest {project}/tests && pytest {project}/tests --gc-on-emit' || 'pytest {project}/tests' }} steps: - uses: actions/checkout@v7 with: @@ -103,30 +101,29 @@ jobs: with: package-dir: ${{ env.SDIST }} - uses: actions/upload-artifact@v7 - id: upload - continue-on-error: true - with: - name: wheels-${{ matrix.os }}-${{ matrix.python }} - path: wheelhouse/*.whl - - uses: actions/upload-artifact@v7 - if: steps.upload.outcome == 'failure' with: name: wheels-${{ matrix.os }}-${{ matrix.python }} path: wheelhouse/*.whl overwrite: true sanitizers: - name: sanitizers (ASan + UBSan, ${{ matrix.arch }}) + name: sanitizers (ASan + UBSan, ${{ matrix.arch }}, ${{ matrix.pass }}) + if: github.event_name != 'push' runs-on: ${{ matrix.runner }} container: quay.io/pypa/manylinux_2_28_${{ matrix.arch }} timeout-minutes: 60 strategy: fail-fast: false matrix: + arch: [x86_64, aarch64] + pass: [plain, gc-on-emit, stress] include: - { arch: x86_64, runner: ubuntu-latest } # also covers the libc++ built from source for arm64 - { arch: aarch64, runner: ubuntu-24.04-arm } + - { pass: plain, args: "" } + - { pass: gc-on-emit, args: --gc-on-emit } + - { pass: stress, args: --stress -k "stress or chaos" } steps: - uses: actions/checkout@v7 # github.workspace is the host's path @@ -135,16 +132,19 @@ jobs: with: path: .wrtc-cache key: libwebrtc-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('cmake/libwebrtc.cmake', 'cmake/libcxx/*.sha256', 'cmake/openh264.*') }} - - run: .github/scripts/sanitizers.sh - # the collector running on libwebrtc threads, whenever they emit (see tests/conftest.py) - - run: .github/scripts/sanitizers.sh --gc-on-emit - # long random sequences of calls (tests/chaos.py) - - run: .github/scripts/sanitizers.sh --stress -k "stress or chaos" + - run: .github/scripts/sanitizers.sh ${{ matrix.args }} sanitizers-macos: - name: sanitizers (ASan + UBSan, macOS) + name: sanitizers (ASan + UBSan, macOS, ${{ matrix.pass }}) + if: github.event_name != 'push' runs-on: macos-15 timeout-minutes: 60 + strategy: + fail-fast: false + matrix: + include: + - { pass: plain, args: "" } + - { pass: gc-on-emit, args: --gc-on-emit } env: WRTC_CACHE_DIR: ${{ github.workspace }}/.wrtc-cache steps: @@ -154,12 +154,11 @@ jobs: path: .wrtc-cache key: libwebrtc-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('cmake/libwebrtc.cmake', 'cmake/libcxx/*.sha256', 'cmake/openh264.*') }} - uses: astral-sh/setup-uv@v7 - - run: .github/scripts/sanitizers-macos.sh - - run: .github/scripts/sanitizers-macos.sh --gc-on-emit + - run: .github/scripts/sanitizers-macos.sh ${{ matrix.args }} publish: if: startsWith(github.ref, 'refs/tags/v') - needs: [lint, sdist, wheels, sanitizers, sanitizers-macos] + needs: [lint, sdist, wheels] runs-on: ubuntu-latest environment: pypi permissions: diff --git a/pyproject.toml b/pyproject.toml index 50cff7d..e63e236 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -99,8 +99,8 @@ build-frontend = "build[uv]" # dev builds stay per file to catch missing includes config-settings = { "cmake.define.CMAKE_UNITY_BUILD" = "ON" } test-groups = ["test"] -# again with a collection on every emit (--gc-on-emit in tests/conftest.py) -test-command = "pytest {project}/tests && pytest {project}/tests --gc-on-emit" +# CI adds --gc-on-emit on PRs (.github/workflows/ci.yml) +test-command = "pytest {project}/tests" [tool.cibuildwheel.linux] archs = ["native"] diff --git a/tests/test_stats.py b/tests/test_stats.py index 9d70d65..62d88b8 100644 --- a/tests/test_stats.py +++ b/tests/test_stats.py @@ -19,6 +19,8 @@ from tests.helpers import connect, stats_of_type, wait_for_event, wait_until, wait_until_unmuted from webrtc.models import rtc_stats +ICE_TYPES = {'candidate-pair', 'local-candidate', 'remote-candidate'} + async def send_audio( caller: webrtc.RTCPeerConnection, callee: webrtc.RTCPeerConnection, stream: webrtc.MediaStream @@ -34,6 +36,11 @@ async def send_audio( return remote +def ice_free_ids(report: webrtc.RTCStatsReport) -> set[str]: + """The ids of the stats in a report, but of its candidate pair and candidates.""" + return {stats.id for stats in report.values() if stats.type not in ICE_TYPES} + + @pytest.mark.asyncio async def test_connection_stats( caller: webrtc.RTCPeerConnection, callee: webrtc.RTCPeerConnection, audio_stream: webrtc.MediaStream @@ -74,7 +81,10 @@ async def has_remote_inbound() -> bool: assert len(stats_of_type(sender_report, 'outbound-rtp')) > 0 assert len(stats_of_type(sender_report, 'inbound-rtp')) == 0 track_report = await caller.get_stats(audio_stream.get_tracks()[0]) - assert set(track_report.keys()) == set(sender_report.keys()) + assert ice_free_ids(track_report) == ice_free_ids(sender_report) + # ICE may still switch the selected pair between the two reports + assert len(stats_of_type(track_report, 'candidate-pair')) == 1 + assert len(stats_of_type(sender_report, 'candidate-pair')) == 1 @pytest.mark.asyncio