|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240 |
- """
- Test fixtures used by DDS in pytest
- """
-
- from pathlib import Path
- import pytest
- import json
- import shutil
- from typing import Sequence, cast, Optional
- from typing_extensions import TypedDict
-
- from _pytest.config import Config as PyTestConfig
- from _pytest.tmpdir import TempPathFactory
- from _pytest.fixtures import FixtureRequest
-
- from dds_ci import toolchain, paths
- from ..dds import DDSWrapper
- from ..util import Pathish
- tc_mod = toolchain
-
-
- def ensure_absent(path: Pathish) -> None:
- path = Path(path)
- if path.is_dir():
- shutil.rmtree(path)
- elif path.exists():
- path.unlink()
- else:
- # File does not exist, wo we are safe to ignore it
- pass
-
-
- class _PackageJSONRequired(TypedDict):
- name: str
- namespace: str
- version: str
-
-
- class PackageJSON(_PackageJSONRequired, total=False):
- depends: Sequence[str]
-
-
- class _LibraryJSONRequired(TypedDict):
- name: str
-
-
- class LibraryJSON(_LibraryJSONRequired, total=False):
- uses: Sequence[str]
-
-
- class Project:
- """
- Utilities to access a project being used as a test.
- """
- def __init__(self, dirpath: Path, dds: DDSWrapper) -> None:
- self.dds = dds.clone()
- self.root = dirpath
- self.build_root = dirpath / '_build'
-
- @property
- def package_json(self) -> PackageJSON:
- """
- Get/set the content of the `package.json` file for the project.
- """
- return cast(PackageJSON, json.loads(self.root.joinpath('package.jsonc').read_text()))
-
- @package_json.setter
- def package_json(self, data: PackageJSON) -> None:
- self.root.joinpath('package.jsonc').write_text(json.dumps(data, indent=2))
-
- @property
- def library_json(self) -> LibraryJSON:
- """
- Get/set the content of the `library.json` file for the project.
- """
- return cast(LibraryJSON, json.loads(self.root.joinpath('library.jsonc').read_text()))
-
- @library_json.setter
- def library_json(self, data: LibraryJSON) -> None:
- self.root.joinpath('library.jsonc').write_text(json.dumps(data, indent=2))
-
- @property
- def project_dir_arg(self) -> str:
- """Argument for --project"""
- return f'--project={self.root}'
-
- def build(self,
- *,
- toolchain: Optional[Pathish] = None,
- timeout: Optional[int] = None,
- tweaks_dir: Optional[Path] = None) -> None:
- """
- Execute 'dds build' on the project
- """
- with tc_mod.fixup_toolchain(toolchain or tc_mod.get_default_test_toolchain()) as tc:
- self.dds.build(root=self.root,
- build_root=self.build_root,
- toolchain=tc,
- timeout=timeout,
- tweaks_dir=tweaks_dir,
- more_args=['-ltrace'])
-
- def compile_file(self, *paths: Pathish, toolchain: Optional[Pathish] = None) -> None:
- with tc_mod.fixup_toolchain(toolchain or tc_mod.get_default_test_toolchain()) as tc:
- self.dds.compile_file(paths, toolchain=tc, out=self.build_root, project_dir=self.root)
-
- def pkg_create(self, *, dest: Optional[Pathish] = None) -> None:
- self.build_root.mkdir(exist_ok=True, parents=True)
- self.dds.run([
- 'pkg',
- 'create',
- self.project_dir_arg,
- f'--out={dest}' if dest else (),
- ], cwd=self.build_root)
-
- def sdist_export(self) -> None:
- self.dds.run(['sdist', 'export', self.dds.cache_dir_arg, self.project_dir_arg])
-
- def write(self, path: Pathish, content: str) -> Path:
- """
- Write the given `content` to `path`. If `path` is relative, it will
- be resolved relative to the root directory of this project.
- """
- path = Path(path)
- if not path.is_absolute():
- path = self.root / path
- path.parent.mkdir(exist_ok=True, parents=True)
- path.write_text(content)
- return path
-
-
- @pytest.fixture()
- def test_parent_dir(request: FixtureRequest) -> Path:
- """
- :class:`pathlib.Path` fixture pointing to the parent directory of the file
- containing the test that is requesting the current fixture
- """
- return Path(request.fspath).parent
-
-
- class ProjectOpener():
- """
- A test fixture that opens project directories for testing
- """
- def __init__(self, dds: DDSWrapper, request: FixtureRequest, worker: str,
- tmp_path_factory: TempPathFactory) -> None:
- self.dds = dds
- self._request = request
- self._worker_id = worker
- self._tmppath_fac = tmp_path_factory
-
- @property
- def test_name(self) -> str:
- """The name of the test that requested this opener"""
- return str(self._request.function.__name__)
-
- @property
- def test_dir(self) -> Path:
- """The directory that contains the test that requested this opener"""
- return Path(self._request.fspath).parent
-
- def open(self, dirpath: Pathish) -> Project:
- """
- Open a new project testing fixture from the given project directory.
-
- :param dirpath: The directory that contains the project to use.
-
- Clones the given directory and then opens a project within that clone.
- The clone directory will be destroyed when the test fixture is torn down.
- """
- dirpath = Path(dirpath)
- if not dirpath.is_absolute():
- dirpath = self.test_dir / dirpath
-
- proj_copy = self.test_dir / '__test_project'
- if self._worker_id != 'master':
- proj_copy = self._tmppath_fac.mktemp('test-project-') / self.test_name
- else:
- self._request.addfinalizer(lambda: ensure_absent(proj_copy))
-
- shutil.copytree(dirpath, proj_copy)
- new_dds = self.dds.clone()
-
- if self._worker_id == 'master':
- repo_dir = self.test_dir / '__test_repo'
- else:
- repo_dir = self._tmppath_fac.mktemp('test-repo-') / self.test_name
-
- new_dds.set_repo_scratch(repo_dir)
- new_dds.default_cwd = proj_copy
- self._request.addfinalizer(lambda: ensure_absent(repo_dir))
-
- return Project(proj_copy, new_dds)
-
-
- @pytest.fixture()
- def project_opener(request: FixtureRequest, worker_id: str, dds: DDSWrapper,
- tmp_path_factory: TempPathFactory) -> ProjectOpener:
- """
- A fixture factory that can open directories as Project objects for building
- and testing. Duplicates the project directory into a temporary location so
- that the original test directory remains unchanged.
- """
- opener = ProjectOpener(dds, request, worker_id, tmp_path_factory)
- return opener
-
-
- @pytest.fixture()
- def tmp_project(request: FixtureRequest, worker_id: str, project_opener: ProjectOpener,
- tmp_path_factory: TempPathFactory) -> Project:
- """
- A fixture that generates an empty temporary project directory that will be thrown away
- when the test completes.
- """
- if worker_id != 'master':
- proj_dir = tmp_path_factory.mktemp('temp-project')
- return project_opener.open(proj_dir)
-
- proj_dir = project_opener.test_dir / '__test_project_empty'
- ensure_absent(proj_dir)
- proj_dir.mkdir()
- proj = project_opener.open(proj_dir)
- request.addfinalizer(lambda: ensure_absent(proj_dir))
- return proj
-
-
- @pytest.fixture(scope='session')
- def dds(dds_exe: Path) -> DDSWrapper:
- """
- A :class:`~dds_ci.dds.DDSWrapper` around the dds executable under test
- """
- wr = DDSWrapper(dds_exe)
- return wr
-
-
- @pytest.fixture(scope='session')
- def dds_exe(pytestconfig: PyTestConfig) -> Path:
- """A :class:`pathlib.Path` pointing to the DDS executable under test"""
- opt = pytestconfig.getoption('--dds-exe') or paths.BUILD_DIR / 'dds'
- return Path(opt)
|