Coverage for src/mafw/devtools/toolchain/tools/pytest.py: 100%
26 statements
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-26 09:13 +0000
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-26 09:13 +0000
1# Copyright 2026 European Union
2# Author: Bulgheroni Antonio (antonio.bulgheroni@ec.europa.eu)
3# SPDX-License-Identifier: EUPL-1.2
4"""
5Concrete :class:`~mafw.devtools.toolchain.ProjectTool` implementation for **pytest**.
7pytest is a project tool whose version lower bound is declared in
8``project.optional-dependencies.test``. This module manages the ``>=``
9specifier for pytest and validates updates by running the full test suite
10via hatch.
12Update logic (inherited from :class:`~mafw.devtools.toolchain.ProjectTool`):
141. Query PyPI for the latest stable pytest release.
152. Compare against the current lower bound in pyproject.toml.
163. If a newer version exists, update the ``>=`` specifier.
18Post-update validation (overridden here):
204. Recreate the ``test`` hatch environment to force fresh dependency resolution.
215. Determine the highest supported Python version from ``tool.mafw.supported-python``.
226. Run the test suite via ``hatch test -py <version> --without-integration --without-slow-integration``.
237. If the test suite fails, revert pyproject.toml to its previous content.
25The revert pattern uses ``_revert()``
26which restores the raw pyproject.toml content saved before the update.
27"""
29from __future__ import annotations
31import subprocess
33from mafw.devtools import DevtoolsError
34from mafw.devtools.toolchain import ProjectTool
37class PytestTool(ProjectTool):
38 """Manage pytest's version lower bound in pyproject.toml.
40 pytest is categorized as a *project* tool because its version is
41 controlled through the ``test`` optional-dependencies group in
42 ``pyproject.toml``. After an update, the ``test`` hatch environment
43 is recreated and the full test suite is executed at the highest
44 supported Python version to validate compatibility. If the tests
45 fail, pyproject.toml is reverted to its pre-update state.
47 All standard behaviour (version detection, update, verify, bootstrap)
48 is provided by :class:`~mafw.devtools.toolchain.ProjectTool`.
49 Only :meth:`post_update` is overridden to run the test suite.
51 :param project_root: Path to the project root directory containing
52 ``pyproject.toml``. Defaults to the current working directory.
53 :type project_root: Path | None
54 """
56 @property
57 def package_name(self) -> str:
58 """PyPI package name.
60 :return: ``"pytest"``
61 :rtype: str
62 """
63 return 'pytest'
65 @property
66 def section_path(self) -> str:
67 """TOML section path where pytest is declared.
69 :return: ``"project.optional-dependencies.test"``
70 :rtype: str
71 """
72 return 'project.optional-dependencies.test'
74 @property
75 def env_name(self) -> str:
76 """Hatch environment where pytest is validated.
78 :return: ``"hatch-test"``
79 :rtype: str
80 """
81 return 'hatch-test'
83 def post_update(self) -> None:
84 """Recreate the test environment, then run the test suite; revert on failure.
86 Steps:
88 1. Force recreation of the ``test`` hatch environment so that the
89 newly specified pytest version is resolved and installed fresh.
90 2. Determine the highest supported Python version dynamically from
91 ``tool.mafw.supported-python`` in pyproject.toml.
92 3. Execute ``hatch test -py <version> --without-integration
93 --without-slow-integration`` from the project root.
94 4. If the test suite fails (non-zero exit code), attempt to extract
95 the conflicting package name via ``_parse_resolution_failure``,
96 revert pyproject.toml, and raise a descriptive error.
98 :raises DevtoolsError: If the test suite fails after the update, or
99 if environment recreation fails due to a resolution conflict.
100 """
101 # Force fresh dependency resolution with the new pytest version.
102 # Determine the highest supported Python version dynamically.
103 py_version = self._get_highest_python_version()
105 # Validate dependency resolution via temporary clone.
106 self._recreate_environment(self.env_name, py_version)
108 # Run the test suite at the highest supported Python version.
109 result = subprocess.run(
110 [ # noqa: S603, S607
111 'hatch',
112 'test',
113 '-py',
114 py_version,
115 '--without-integration',
116 '--without-slow-integration',
117 ],
118 capture_output=True,
119 text=True,
120 check=False,
121 cwd=self._project_root,
122 )
124 if result.returncode != 0:
125 # Try to identify which package caused a resolution failure.
126 conflict = self._parse_resolution_failure(result.stderr)
127 # Revert pyproject.toml to its pre-update state.
128 self._revert()
129 msg = f'Test suite failed after updating {self.name} — pyproject.toml has been reverted.'
130 if conflict:
131 msg += f'\nConflicting package: {conflict}'
132 msg += f'\n{result.stderr.strip()}'
133 raise DevtoolsError(msg)