You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

200 lines
6.1KB

  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, NewDDSWrapper
  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. def __init__(self, dirpath: Path, dds: DDSWrapper) -> None:
  38. self.dds = dds
  39. self.root = dirpath
  40. self.build_root = dirpath / '_build'
  41. @property
  42. def package_json(self) -> PackageJSON:
  43. return cast(PackageJSON, json.loads(self.root.joinpath('package.jsonc').read_text()))
  44. @package_json.setter
  45. def package_json(self, data: PackageJSON) -> None:
  46. self.root.joinpath('package.jsonc').write_text(json.dumps(data, indent=2))
  47. @property
  48. def library_json(self) -> LibraryJSON:
  49. return cast(LibraryJSON, json.loads(self.root.joinpath('library.jsonc').read_text()))
  50. @library_json.setter
  51. def library_json(self, data: LibraryJSON) -> None:
  52. self.root.joinpath('library.jsonc').write_text(json.dumps(data, indent=2))
  53. @property
  54. def project_dir_arg(self) -> str:
  55. """Argument for --project"""
  56. return f'--project={self.root}'
  57. def build(self,
  58. *,
  59. toolchain: Optional[Pathish] = None,
  60. timeout: Optional[int] = None,
  61. tweaks_dir: Optional[Path] = None) -> None:
  62. """
  63. Execute 'dds build' on the project
  64. """
  65. with tc_mod.fixup_toolchain(toolchain or tc_mod.get_default_test_toolchain()) as tc:
  66. self.dds.build(root=self.root,
  67. build_root=self.build_root,
  68. toolchain=tc,
  69. timeout=timeout,
  70. tweaks_dir=tweaks_dir,
  71. more_args=['-ltrace'])
  72. def compile_file(self, *paths: Pathish, toolchain: Optional[Pathish] = None) -> None:
  73. with tc_mod.fixup_toolchain(toolchain or tc_mod.get_default_test_toolchain()) as tc:
  74. self.dds.compile_file(paths, toolchain=tc, out=self.build_root, project_dir=self.root)
  75. def pkg_create(self, *, dest: Optional[Pathish] = None) -> None:
  76. self.build_root.mkdir(exist_ok=True, parents=True)
  77. self.dds.run([
  78. 'pkg',
  79. 'create',
  80. self.project_dir_arg,
  81. f'--out={dest}' if dest else (),
  82. ], cwd=self.build_root)
  83. def sdist_export(self) -> None:
  84. self.dds.run(['sdist', 'export', self.dds.cache_dir_arg, self.project_dir_arg])
  85. def write(self, path: Pathish, content: str) -> Path:
  86. path = Path(path)
  87. if not path.is_absolute():
  88. path = self.root / path
  89. path.parent.mkdir(exist_ok=True, parents=True)
  90. path.write_text(content)
  91. return path
  92. @pytest.fixture()
  93. def test_parent_dir(request: FixtureRequest) -> Path:
  94. return Path(request.fspath).parent
  95. class ProjectOpener():
  96. def __init__(self, dds: DDSWrapper, request: FixtureRequest, worker: str,
  97. tmp_path_factory: TempPathFactory) -> None:
  98. self.dds = dds
  99. self._request = request
  100. self._worker_id = worker
  101. self._tmppath_fac = tmp_path_factory
  102. @property
  103. def test_name(self) -> str:
  104. """The name of the test that requested this opener"""
  105. return str(self._request.function.__name__)
  106. @property
  107. def test_dir(self) -> Path:
  108. """The directory that contains the test that requested this opener"""
  109. return Path(self._request.fspath).parent
  110. def open(self, dirpath: Pathish) -> Project:
  111. dirpath = Path(dirpath)
  112. if not dirpath.is_absolute():
  113. dirpath = self.test_dir / dirpath
  114. proj_copy = self.test_dir / '__test_project'
  115. if self._worker_id != 'master':
  116. proj_copy = self._tmppath_fac.mktemp('test-project-') / self.test_name
  117. else:
  118. self._request.addfinalizer(lambda: ensure_absent(proj_copy))
  119. shutil.copytree(dirpath, proj_copy)
  120. new_dds = self.dds.clone()
  121. if self._worker_id == 'master':
  122. repo_dir = self.test_dir / '__test_repo'
  123. else:
  124. repo_dir = self._tmppath_fac.mktemp('test-repo-') / self.test_name
  125. new_dds.set_repo_scratch(repo_dir)
  126. new_dds.default_cwd = proj_copy
  127. self._request.addfinalizer(lambda: ensure_absent(repo_dir))
  128. return Project(proj_copy, new_dds)
  129. @pytest.fixture()
  130. def project_opener(request: FixtureRequest, worker_id: str, dds: DDSWrapper,
  131. tmp_path_factory: TempPathFactory) -> ProjectOpener:
  132. opener = ProjectOpener(dds, request, worker_id, tmp_path_factory)
  133. return opener
  134. @pytest.fixture()
  135. def tmp_project(request: FixtureRequest, worker_id: str, project_opener: ProjectOpener,
  136. tmp_path_factory: TempPathFactory) -> Project:
  137. if worker_id != 'master':
  138. proj_dir = tmp_path_factory.mktemp('temp-project')
  139. return project_opener.open(proj_dir)
  140. proj_dir = project_opener.test_dir / '__test_project_empty'
  141. ensure_absent(proj_dir)
  142. proj_dir.mkdir()
  143. proj = project_opener.open(proj_dir)
  144. request.addfinalizer(lambda: ensure_absent(proj_dir))
  145. return proj
  146. @pytest.fixture(scope='session')
  147. def dds(dds_exe: Path) -> NewDDSWrapper:
  148. wr = NewDDSWrapper(dds_exe)
  149. return wr
  150. @pytest.fixture(scope='session')
  151. def dds_exe(pytestconfig: PyTestConfig) -> Path:
  152. opt = pytestconfig.getoption('--dds-exe') or paths.BUILD_DIR / 'dds'
  153. return Path(opt)