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

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**. 

6 

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. 

11 

12Update logic (inherited from :class:`~mafw.devtools.toolchain.ProjectTool`): 

13 

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. 

17 

18Post-update validation (overridden here): 

19 

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. 

24 

25The revert pattern uses ``_revert()`` 

26which restores the raw pyproject.toml content saved before the update. 

27""" 

28 

29from __future__ import annotations 

30 

31import subprocess 

32 

33from mafw.devtools import DevtoolsError 

34from mafw.devtools.toolchain import ProjectTool 

35 

36 

37class PytestTool(ProjectTool): 

38 """Manage pytest's version lower bound in pyproject.toml. 

39 

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. 

46 

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. 

50 

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 """ 

55 

56 @property 

57 def package_name(self) -> str: 

58 """PyPI package name. 

59 

60 :return: ``"pytest"`` 

61 :rtype: str 

62 """ 

63 return 'pytest' 

64 

65 @property 

66 def section_path(self) -> str: 

67 """TOML section path where pytest is declared. 

68 

69 :return: ``"project.optional-dependencies.test"`` 

70 :rtype: str 

71 """ 

72 return 'project.optional-dependencies.test' 

73 

74 @property 

75 def env_name(self) -> str: 

76 """Hatch environment where pytest is validated. 

77 

78 :return: ``"hatch-test"`` 

79 :rtype: str 

80 """ 

81 return 'hatch-test' 

82 

83 def post_update(self) -> None: 

84 """Recreate the test environment, then run the test suite; revert on failure. 

85 

86 Steps: 

87 

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. 

97 

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() 

104 

105 # Validate dependency resolution via temporary clone. 

106 self._recreate_environment(self.env_name, py_version) 

107 

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 ) 

123 

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)