Skip to content

Commit

Permalink
Pytest runtime optimization
Browse files Browse the repository at this point in the history
  • Loading branch information
cdce8p committed Dec 14, 2024
1 parent ee01029 commit e49ea2e
Show file tree
Hide file tree
Showing 4 changed files with 99 additions and 2 deletions.
67 changes: 66 additions & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ env:
CACHE_VERSION: 11-c1
UV_CACHE_VERSION: 1
MYPY_CACHE_VERSION: 9
PYTEST_RUNTIME_CACHE_VERSION: 1
HA_SHORT_VERSION: "2025.1"
DEFAULT_PYTHON: "3.12"
ALL_PYTHON_VERSIONS: "['3.12', '3.13']"
Expand Down Expand Up @@ -934,6 +935,21 @@ jobs:
key: >-
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.python_cache_key }}
- name: Generate partial pytest runtime restore key
id: generate-pytest-runtime-key
run: |
echo "key=pytest-runtime-${{ env.PYTEST_RUNTIME_CACHE_VERSION }}-${{
env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
- name: Restore pytest runtime cache
uses: actions/cache/[email protected]
with:
path: pytest-time-report-${{ env.DEFAULT_PYTHON }}.json
key: >-
${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-${{
steps.generate-pytest-runtime-key.outputs.key }}
restore-keys: |
${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-pytest-runtime-${{
env.PYTEST_RUNTIME_CACHE_VERSION }}-${{ env.HA_SHORT_VERSION }}-
- name: Run split_tests.py
run: |
. venv/bin/activate
Expand All @@ -944,6 +960,12 @@ jobs:
name: pytest_buckets
path: pytest_buckets.txt
overwrite: true
- name: Upload pytest collect output
uses: actions/upload-artifact@v4
with:
name: pytest-${{ github.run_number }}-collection
path: pytest-collect-output.txt
overwrite: true

pytest-full:
runs-on: ubuntu-24.04
Expand Down Expand Up @@ -1035,14 +1057,17 @@ jobs:
${cov_params[@]} \
-o console_output_style=count \
-p no:sugar \
--time-report-name pytest-time-report-${{ matrix.python-version }}-${{ matrix.group }}.json \
$(sed -n "${{ matrix.group }},1p" pytest_buckets.txt) \
2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt
- name: Upload pytest output
if: success() || failure() && steps.pytest-full.conclusion == 'failure'
uses: actions/[email protected]
with:
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ matrix.group }}
path: pytest-*.txt
path: |
pytest-*.txt
pytest-*.json
overwrite: true
- name: Upload coverage artifact
if: needs.info.outputs.skip_coverage != 'true'
Expand All @@ -1057,6 +1082,45 @@ jobs:
run: |
./script/check_dirty
pytest-time:
runs-on: ubuntu-24.04
needs:
- info
- pytest-full
strategy:
fail-fast: false
matrix:
python-version: ${{ fromJSON(needs.info.outputs.python_versions) }}
name: Combine test runtimes
steps:
- name: Generate partial pytest runtime restore key
id: generate-pytest-runtime-key
run: |
echo "key=pytest-runtime-${{ env.PYTEST_RUNTIME_CACHE_VERSION }}-${{
env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
- name: Download pytest runtime artifacts
uses: actions/[email protected]
with:
pattern: pytest-${{ github.run_number }}-${{ matrix.python-version }}-*
merge-multiple: true
- name: Combine files into one
run: |
jq 'reduce inputs as $item ({}; . *= $item)' \
pytest-time-report-${{ matrix.python-version }}-*.json \
> pytest-time-report-${{ matrix.python-version }}.json
- name: Upload combined pytest runtime artifact
uses: actions/[email protected]
with:
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-time-report
path: pytest-time-report-${{ matrix.python-version }}.json
- name: Save pytest runtime cache
uses: actions/cache/[email protected]
with:
path: pytest-time-report-${{ matrix.python-version }}.json
key: >-
${{ runner.os }}-${{ matrix.python-version }}-${{
steps.generate-pytest-runtime-key.outputs.key }}
pytest-mariadb:
runs-on: ubuntu-24.04
services:
Expand Down Expand Up @@ -1487,6 +1551,7 @@ jobs:
- audit-licenses
- installed-packages
- pytest-full
- pytest-time
timeout-minutes: 5
steps:
- name: Merge all pytest coverage artifacts
Expand Down
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -136,4 +136,5 @@ tmp_cache
.ropeproject

# Will be created from script/split_tests.py
pytest_buckets.txt
pytest_buckets.txt
pytest-time-report*.json
2 changes: 2 additions & 0 deletions script/split_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,8 @@ def collect_tests(path: Path) -> TestFolder:

folder = TestFolder(path)

Path("pytest-collect-output.txt").write_text(result.stdout)

for line in result.stdout.splitlines():
if not line.strip():
continue
Expand Down
29 changes: 29 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@
import functools
import gc
import itertools
import json
import logging
import os
from pathlib import Path
import reprlib
from shutil import rmtree
import sqlite3
Expand All @@ -19,6 +21,7 @@
from typing import TYPE_CHECKING, Any, cast
from unittest.mock import AsyncMock, MagicMock, Mock, _patch, patch

from _pytest.terminal import TerminalReporter
from aiohttp import client
from aiohttp.test_utils import (
BaseTestServer,
Expand Down Expand Up @@ -140,6 +143,30 @@
def pytest_addoption(parser: pytest.Parser) -> None:
"""Register custom pytest options."""
parser.addoption("--dburl", action="store", default="sqlite://")
parser.addoption(
"--time-report-name", action="store", default="pytest-time-report.json"
)


class PytestExecutionTimer: # noqa: D101
def pytest_terminal_summary( # noqa: D102
self,
terminalreporter: TerminalReporter,
exitstatus: pytest.ExitCode,
config: pytest.Config,
) -> None:
if config.option.collectonly:
return
raw_data: dict[str, list[float]] = {}
for replist in terminalreporter.stats.values():
for rep in replist:
if isinstance(rep, pytest.TestReport):
raw_data.setdefault(rep.location[0], []).append(rep.duration)
data = {filename: sum(values) for filename, values in raw_data.items()}
time_report_filename = config.option.time_report_name
file = Path(__file__).parents[1].joinpath(time_report_filename)
with open(file, "w", encoding="utf-8") as fp:
json.dump(data, fp, indent=2)


def pytest_configure(config: pytest.Config) -> None:
Expand All @@ -155,6 +182,8 @@ def pytest_configure(config: pytest.Config) -> None:
# See https://github.com/syrupy-project/syrupy/pull/901
SnapshotSession.finish = override_syrupy_finish

config.pluginmanager.register(PytestExecutionTimer())


def pytest_runtest_setup() -> None:
"""Prepare pytest_socket and freezegun.
Expand Down

0 comments on commit e49ea2e

Please sign in to comment.