Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

241 line
7.6KB

  1. """
  2. Test fixtures used by DDS in pytest
  3. """
  4. from pathlib import Path
  5. import pytest
  6. import json
  7. import shutil
  8. from typing import Sequence, cast, Optional
  9. from typing_extensions import TypedDict
  10. from _pytest.config import Config as PyTestConfig
  11. from _pytest.tmpdir import TempPathFactory
  12. from _pytest.fixtures import FixtureRequest
  13. from dds_ci import toolchain, paths
  14. from ..dds import DDSWrapper
  15. from ..util import Pathish
  16. tc_mod = toolchain
  17. def ensure_absent(path: Pathish) -> None:
  18. path = Path(path)
  19. if path.is_dir():
  20. shutil.rmtree(path)
  21. elif path.exists():
  22. path.unlink()
  23. else:
  24. # File does not exist, wo we are safe to ignore it
  25. pass
  26. class _PackageJSONRequired(TypedDict):
  27. name: str
  28. namespace: str
  29. version: str
  30. class PackageJSON(_PackageJSONRequired, total=False):
  31. depends: Sequence[str]
  32. class _LibraryJSONRequired(TypedDict):
  33. name: str
  34. class LibraryJSON(_LibraryJSONRequired, total=False):
  35. uses: Sequence[str]
  36. class Project:
  37. """
  38. Utilities to access a project being used as a test.
  39. """
  40. def __init__(self, dirpath: Path, dds: DDSWrapper) -> None:
  41. self.dds = dds.clone()
  42. self.root = dirpath
  43. self.build_root = dirpath / '_build'
  44. @property
  45. def package_json(self) -> PackageJSON:
  46. """
  47. Get/set the content of the `package.json` file for the project.
  48. """
  49. return cast(PackageJSON, json.loads(self.root.joinpath('package.jsonc').read_text()))
  50. @package_json.setter
  51. def package_json(self, data: PackageJSON) -> None:
  52. self.root.joinpath('package.jsonc').write_text(json.dumps(data, indent=2))
  53. @property
  54. def library_json(self) -> LibraryJSON:
  55. """
  56. Get/set the content of the `library.json` file for the project.
  57. """
  58. return cast(LibraryJSON, json.loads(self.root.joinpath('library.jsonc').read_text()))
  59. @library_json.setter
  60. def library_json(self, data: LibraryJSON) -> None:
  61. self.root.joinpath('library.jsonc').write_text(json.dumps(data, indent=2))
  62. @property
  63. def project_dir_arg(self) -> str:
  64. """Argument for --project"""
  65. return f'--project={self.root}'
  66. def build(self,
  67. *,
  68. toolchain: Optional[Pathish] = None,
  69. timeout: Optional[int] = None,
  70. tweaks_dir: Optional[Path] = None) -> None:
  71. """
  72. Execute 'dds build' on the project
  73. """
  74. with tc_mod.fixup_toolchain(toolchain or tc_mod.get_default_test_toolchain()) as tc:
  75. self.dds.build(root=self.root,
  76. build_root=self.build_root,
  77. toolchain=tc,
  78. timeout=timeout,
  79. tweaks_dir=tweaks_dir,
  80. more_args=['-ltrace'])
  81. def compile_file(self, *paths: Pathish, toolchain: Optional[Pathish] = None) -> None:
  82. with tc_mod.fixup_toolchain(toolchain or tc_mod.get_default_test_toolchain()) as tc:
  83. self.dds.compile_file(paths, toolchain=tc, out=self.build_root, project_dir=self.root)
  84. def pkg_create(self, *, dest: Optional[Pathish] = None) -> None:
  85. self.build_root.mkdir(exist_ok=True, parents=True)
  86. self.dds.run([
  87. 'pkg',
  88. 'create',
  89. self.project_dir_arg,
  90. f'--out={dest}' if dest else (),
  91. ], cwd=self.build_root)
  92. def sdist_export(self) -> None:
  93. self.dds.run(['sdist', 'export', self.dds.cache_dir_arg, self.project_dir_arg])
  94. def write(self, path: Pathish, content: str) -> Path:
  95. """
  96. Write the given `content` to `path`. If `path` is relative, it will
  97. be resolved relative to the root directory of this project.
  98. """
  99. path = Path(path)
  100. if not path.is_absolute():
  101. path = self.root / path
  102. path.parent.mkdir(exist_ok=True, parents=True)
  103. path.write_text(content)
  104. return path
  105. @pytest.fixture()
  106. def test_parent_dir(request: FixtureRequest) -> Path:
  107. """
  108. :class:`pathlib.Path` fixture pointing to the parent directory of the file
  109. containing the test that is requesting the current fixture
  110. """
  111. return Path(request.fspath).parent
  112. class ProjectOpener():
  113. """
  114. A test fixture that opens project directories for testing
  115. """
  116. def __init__(self, dds: DDSWrapper, request: FixtureRequest, worker: str,
  117. tmp_path_factory: TempPathFactory) -> None:
  118. self.dds = dds
  119. self._request = request
  120. self._worker_id = worker
  121. self._tmppath_fac = tmp_path_factory
  122. @property
  123. def test_name(self) -> str:
  124. """The name of the test that requested this opener"""
  125. return str(self._request.function.__name__)
  126. @property
  127. def test_dir(self) -> Path:
  128. """The directory that contains the test that requested this opener"""
  129. return Path(self._request.fspath).parent
  130. def open(self, dirpath: Pathish) -> Project:
  131. """
  132. Open a new project testing fixture from the given project directory.
  133. :param dirpath: The directory that contains the project to use.
  134. Clones the given directory and then opens a project within that clone.
  135. The clone directory will be destroyed when the test fixture is torn down.
  136. """
  137. dirpath = Path(dirpath)
  138. if not dirpath.is_absolute():
  139. dirpath = self.test_dir / dirpath
  140. proj_copy = self.test_dir / '__test_project'
  141. if self._worker_id != 'master':
  142. proj_copy = self._tmppath_fac.mktemp('test-project-') / self.test_name
  143. else:
  144. self._request.addfinalizer(lambda: ensure_absent(proj_copy))
  145. shutil.copytree(dirpath, proj_copy)
  146. new_dds = self.dds.clone()
  147. if self._worker_id == 'master':
  148. repo_dir = self.test_dir / '__test_repo'
  149. else:
  150. repo_dir = self._tmppath_fac.mktemp('test-repo-') / self.test_name
  151. new_dds.set_repo_scratch(repo_dir)
  152. new_dds.default_cwd = proj_copy
  153. self._request.addfinalizer(lambda: ensure_absent(repo_dir))
  154. return Project(proj_copy, new_dds)
  155. @pytest.fixture()
  156. def project_opener(request: FixtureRequest, worker_id: str, dds: DDSWrapper,
  157. tmp_path_factory: TempPathFactory) -> ProjectOpener:
  158. """
  159. A fixture factory that can open directories as Project objects for building
  160. and testing. Duplicates the project directory into a temporary location so
  161. that the original test directory remains unchanged.
  162. """
  163. opener = ProjectOpener(dds, request, worker_id, tmp_path_factory)
  164. return opener
  165. @pytest.fixture()
  166. def tmp_project(request: FixtureRequest, worker_id: str, project_opener: ProjectOpener,
  167. tmp_path_factory: TempPathFactory) -> Project:
  168. """
  169. A fixture that generates an empty temporary project directory that will be thrown away
  170. when the test completes.
  171. """
  172. if worker_id != 'master':
  173. proj_dir = tmp_path_factory.mktemp('temp-project')
  174. return project_opener.open(proj_dir)
  175. proj_dir = project_opener.test_dir / '__test_project_empty'
  176. ensure_absent(proj_dir)
  177. proj_dir.mkdir()
  178. proj = project_opener.open(proj_dir)
  179. request.addfinalizer(lambda: ensure_absent(proj_dir))
  180. return proj
  181. @pytest.fixture(scope='session')
  182. def dds(dds_exe: Path) -> DDSWrapper:
  183. """
  184. A :class:`~dds_ci.dds.DDSWrapper` around the dds executable under test
  185. """
  186. wr = DDSWrapper(dds_exe)
  187. return wr
  188. @pytest.fixture(scope='session')
  189. def dds_exe(pytestconfig: PyTestConfig) -> Path:
  190. """A :class:`pathlib.Path` pointing to the DDS executable under test"""
  191. opt = pytestconfig.getoption('--dds-exe') or paths.BUILD_DIR / 'dds'
  192. return Path(opt)