| from .dds import DDS, DDSFixtureParams, scoped_dds, dds_fixture_conf, dds_fixture_conf_1 |
| from contextlib import ExitStack | |||||
| from tests import DDS | |||||
| from tests.fileutil import set_contents | |||||
| def test_lib_with_just_app(dds: DDS, scope: ExitStack): | |||||
| scope.enter_context( | |||||
| set_contents( | |||||
| dds.source_root / 'src/foo.main.cpp', | |||||
| b'int main() {}', | |||||
| )) | |||||
| dds.build() | |||||
| assert (dds.build_dir / f'foo{dds.exe_suffix}').is_file() |
| from contextlib import contextmanager | |||||
| from tests import DDS | |||||
| from tests.fileutil import ensure_dir, set_contents | |||||
| def test_build_empty(dds: DDS): | |||||
| assert not dds.source_root.exists() | |||||
| dds.scope.enter_context(ensure_dir(dds.source_root)) | |||||
| dds.build() | |||||
| def test_build_simple(dds: DDS): | |||||
| dds.scope.enter_context( | |||||
| set_contents(dds.source_root / 'src/f.cpp', b'void foo() {}')) | |||||
| dds.build() | |||||
| def basic_pkg_dds(dds: DDS): | |||||
| return set_contents( | |||||
| dds.source_root / 'package.dds', b''' | |||||
| Name: test-pkg | |||||
| Version: 0.2.2 | |||||
| ''') | |||||
| def test_empty_with_pkg_dds(dds: DDS): | |||||
| dds.scope.enter_context(basic_pkg_dds(dds)) | |||||
| dds.build() | |||||
| def test_empty_with_lib_dds(dds: DDS): | |||||
| dds.scope.enter_context(basic_pkg_dds(dds)) | |||||
| dds.build() | |||||
| pass | |||||
| def test_empty_sdist_create(dds: DDS): | |||||
| dds.scope.enter_context(basic_pkg_dds(dds)) | |||||
| dds.sdist_create() | |||||
| def test_empty_sdist_export(dds: DDS): | |||||
| dds.scope.enter_context(basic_pkg_dds(dds)) | |||||
| dds.sdist_export() |
| from contextlib import ExitStack | |||||
| from tests import DDS | |||||
| from tests.fileutil import set_contents | |||||
| def test_simple_lib(dds: DDS, scope: ExitStack): | |||||
| scope.enter_context( | |||||
| dds.set_contents( | |||||
| 'src/foo.cpp', | |||||
| b'int the_answer() { return 42; }', | |||||
| )) | |||||
| scope.enter_context( | |||||
| dds.set_contents( | |||||
| 'library.dds', | |||||
| b'Name: TestLibrary', | |||||
| )) | |||||
| scope.enter_context( | |||||
| dds.set_contents( | |||||
| 'package.dds', | |||||
| b''' | |||||
| Name: TestProject | |||||
| Version: 0.0.0 | |||||
| ''', | |||||
| )) | |||||
| dds.build(tests=True, apps=False, warnings=False, export=True) | |||||
| assert (dds.build_dir / 'compile_commands.json').is_file() | |||||
| assert list(dds.build_dir.glob('libTestLibrary*')) != [] |
| from contextlib import ExitStack | |||||
| from tests import DDS | |||||
| from tests.fileutil import set_contents | |||||
| def test_lib_with_just_test(dds: DDS, scope: ExitStack): | |||||
| scope.enter_context( | |||||
| set_contents( | |||||
| dds.source_root / 'src/foo.test.cpp', | |||||
| b'int main() {}', | |||||
| )) | |||||
| dds.build(tests=True, apps=False, warnings=False, export=False) | |||||
| assert (dds.build_dir / f'test/foo{dds.exe_suffix}').is_file() |
| from contextlib import ExitStack | |||||
| from typing import Optional | |||||
| from pathlib import Path | |||||
| import shutil | |||||
| import pytest | |||||
| from tests import scoped_dds, DDSFixtureParams | |||||
| @pytest.yield_fixture | |||||
| def dds(request, tmp_path: Path, worker_id: str, scope: ExitStack): | |||||
| test_source_dir = Path(request.fspath).absolute().parent | |||||
| test_root = test_source_dir | |||||
| # If we are running in parallel, use a unique directory as scratch | |||||
| # space so that we aren't stomping on anyone else | |||||
| if worker_id != 'master': | |||||
| test_root = tmp_path / request.function.__name__ | |||||
| shutil.copytree(test_source_dir, test_root) | |||||
| project_dir = test_root / 'project' | |||||
| # Check if we have a special configuration | |||||
| if hasattr(request, 'param'): | |||||
| assert isinstance(request.param, DDSFixtureParams), \ | |||||
| ('Using the `dds` fixture requires passing in indirect ' | |||||
| 'params. Use @dds_fixture_conf to configure the fixture') | |||||
| params: DDSFixtureParams = request.param | |||||
| project_dir = test_root / params.subdir | |||||
| # Create the instance. Auto-clean when we're done | |||||
| yield scope.enter_context(scoped_dds(test_root, project_dir, request.function.__name__)) | |||||
| @pytest.fixture | |||||
| def scope(): | |||||
| with ExitStack() as scope: | |||||
| yield scope |
| import os | |||||
| import itertools | |||||
| from contextlib import contextmanager, ExitStack | |||||
| from pathlib import Path | |||||
| from typing import Iterable, Union, Any, Dict, NamedTuple, ContextManager | |||||
| import subprocess | |||||
| import shutil | |||||
| import pytest | |||||
| from . import fileutil | |||||
| CommandLineArg = Union[str, Path, int, float] | |||||
| CommandLineArg1 = Union[CommandLineArg, Iterable[CommandLineArg]] | |||||
| CommandLineArg2 = Union[CommandLineArg1, Iterable[CommandLineArg1]] | |||||
| CommandLineArg3 = Union[CommandLineArg2, Iterable[CommandLineArg2]] | |||||
| CommandLineArg4 = Union[CommandLineArg3, Iterable[CommandLineArg3]] | |||||
| CommandLine = Iterable[CommandLineArg4] | |||||
| def _flatten_cmd(cmd: CommandLine) -> Iterable[str]: | |||||
| if isinstance(cmd, (str, Path)): | |||||
| yield str(cmd) | |||||
| elif isinstance(cmd, (int, float)): | |||||
| yield str(cmd) | |||||
| elif hasattr(cmd, '__iter__'): | |||||
| each = (_flatten_cmd(arg) for arg in cmd) # type: ignore | |||||
| for item in each: | |||||
| yield from item | |||||
| else: | |||||
| assert False, f'Invalid command line element: {repr(cmd)}' | |||||
| class DDS: | |||||
| def __init__(self, dds_exe: Path, test_dir: Path, project_dir: Path, | |||||
| scope: ExitStack) -> None: | |||||
| self.dds_exe = dds_exe | |||||
| self.test_dir = test_dir | |||||
| self.source_root = project_dir | |||||
| self.scratch_dir = project_dir / '_test_scratch' | |||||
| self.scope = scope | |||||
| self.scope.callback(self.cleanup) | |||||
| @property | |||||
| def repo_dir(self) -> Path: | |||||
| return self.scratch_dir / 'repo' | |||||
| @property | |||||
| def deps_build_dir(self) -> Path: | |||||
| return self.scratch_dir / 'deps-build' | |||||
| @property | |||||
| def build_dir(self) -> Path: | |||||
| return self.scratch_dir / 'build' | |||||
| @property | |||||
| def lmi_path(self) -> Path: | |||||
| return self.scratch_dir / 'INDEX.lmi' | |||||
| def cleanup(self): | |||||
| if self.scratch_dir.exists(): | |||||
| shutil.rmtree(self.scratch_dir) | |||||
| def run_unchecked(self, cmd: CommandLine, *, | |||||
| cwd: Path = None) -> subprocess.CompletedProcess: | |||||
| full_cmd = list(_flatten_cmd(itertools.chain([self.dds_exe], cmd))) | |||||
| return subprocess.run( | |||||
| full_cmd, | |||||
| cwd=cwd or self.source_root, | |||||
| # stdout=subprocess.PIPE, | |||||
| # stderr=subprocess.STDOUT, | |||||
| ) | |||||
| def run(self, cmd: CommandLine, *, | |||||
| cwd: Path = None) -> subprocess.CompletedProcess: | |||||
| cmdline = list(_flatten_cmd(cmd)) | |||||
| res = self.run_unchecked(cmd) | |||||
| if res.returncode != 0: | |||||
| raise subprocess.CalledProcessError( | |||||
| res.returncode, [self.dds_exe] + cmdline, res.stdout) | |||||
| return res | |||||
| @property | |||||
| def repo_dir_arg(self) -> str: | |||||
| return f'--repo-dir={self.repo_dir}' | |||||
| @property | |||||
| def project_dir_arg(self) -> str: | |||||
| return f'--project-dir={self.source_root}' | |||||
| def deps_ls(self) -> subprocess.CompletedProcess: | |||||
| return self.run(['deps', 'ls']) | |||||
| def deps_get(self) -> subprocess.CompletedProcess: | |||||
| return self.run([ | |||||
| 'deps', | |||||
| 'get', | |||||
| self.repo_dir_arg, | |||||
| ]) | |||||
| def deps_build(self, *, | |||||
| toolchain: str = None) -> subprocess.CompletedProcess: | |||||
| return self.run([ | |||||
| 'deps', | |||||
| 'build', | |||||
| f'--toolchain={toolchain or self.default_builtin_toolchain}', | |||||
| self.repo_dir_arg, | |||||
| f'--deps-build-dir={self.deps_build_dir}', | |||||
| f'--lmi-path={self.lmi_path}', | |||||
| ]) | |||||
| def build(self, | |||||
| *, | |||||
| toolchain: str = None, | |||||
| apps: bool = True, | |||||
| warnings: bool = True, | |||||
| tests: bool = True, | |||||
| export: bool = False) -> subprocess.CompletedProcess: | |||||
| return self.run([ | |||||
| 'build', | |||||
| f'--out={self.build_dir}', | |||||
| ['--tests'] if tests else [], | |||||
| ['--apps'] if apps else [], | |||||
| ['--warnings'] if warnings else [], | |||||
| ['--export'] if export else [], | |||||
| f'--toolchain={toolchain or self.default_builtin_toolchain}', | |||||
| self.project_dir_arg, | |||||
| ]) | |||||
| def sdist_create(self) -> subprocess.CompletedProcess: | |||||
| return self.run([ | |||||
| 'sdist', | |||||
| 'create', | |||||
| self.project_dir_arg, | |||||
| f'--out={self.build_dir / "stuff.sds"}', | |||||
| ]) | |||||
| def sdist_export(self) -> subprocess.CompletedProcess: | |||||
| return self.run([ | |||||
| 'sdist', | |||||
| 'export', | |||||
| self.project_dir_arg, | |||||
| self.repo_dir_arg, | |||||
| ]) | |||||
| @property | |||||
| def default_builtin_toolchain(self) -> str: | |||||
| if os.name == 'posix': | |||||
| return ':gcc-8' | |||||
| elif os.name == 'nt': | |||||
| return ':msvc' | |||||
| else: | |||||
| raise RuntimeError( | |||||
| f'No default builtin toolchain defined for tests on platform "{os.name}"' | |||||
| ) | |||||
| @property | |||||
| def exe_suffix(self) -> str: | |||||
| if os.name == 'posix': | |||||
| return '' | |||||
| elif os.name == 'nt': | |||||
| return '.exe' | |||||
| else: | |||||
| raise RuntimeError( | |||||
| f'We don\'t know the executable suffix for the platform "{os.name}"' | |||||
| ) | |||||
| def set_contents(self, path: Union[str, Path], | |||||
| content: bytes) -> ContextManager[Path]: | |||||
| return fileutil.set_contents(self.source_root / path, content) | |||||
| @contextmanager | |||||
| def scoped_dds(test_dir: Path, project_dir: Path, name: str): | |||||
| dds_exe = Path(__file__).absolute().parent.parent / '_build/dds' | |||||
| if os.name == 'nt': | |||||
| dds_exe = dds_exe.with_suffix('.exe') | |||||
| with ExitStack() as scope: | |||||
| yield DDS(dds_exe, test_dir, project_dir, scope) | |||||
| class DDSFixtureParams(NamedTuple): | |||||
| ident: str | |||||
| subdir: Union[Path, str] | |||||
| def dds_fixture_conf(*argsets: DDSFixtureParams): | |||||
| args = list(argsets) | |||||
| return pytest.mark.parametrize( | |||||
| 'dds', args, indirect=True, ids=[p.ident for p in args]) | |||||
| def dds_fixture_conf_1(subdir: Union[Path, str]): | |||||
| params = DDSFixtureParams(ident='only', subdir=subdir) | |||||
| return pytest.mark.parametrize('dds', [params], indirect=True, ids=['.']) |
| import pytest | |||||
| import subprocess | |||||
| from tests import DDS, DDSFixtureParams, dds_fixture_conf | |||||
| dds_conf = dds_fixture_conf( | |||||
| DDSFixtureParams(ident='git-remote', subdir='git-remote'), | |||||
| DDSFixtureParams(ident='no-deps', subdir='no-deps'), | |||||
| ) | |||||
| @dds_conf | |||||
| def test_ls(dds: DDS): | |||||
| dds.run(['deps', 'ls']) | |||||
| @dds_conf | |||||
| def test_deps_build(dds: DDS): | |||||
| assert not dds.repo_dir.exists() | |||||
| dds.deps_get() | |||||
| assert dds.repo_dir.exists(), '`deps get` did not generate a repo directory' | |||||
| assert not dds.lmi_path.exists() | |||||
| dds.deps_build() | |||||
| assert dds.lmi_path.exists(), '`deps build` did not generate the build dir' |
| Name: deps-test | |||||
| Version: 0.0.0 | |||||
| Depends: neo-buffer 0.1.0 |
| Remote-Package: neo-buffer 0.1.0; git url=git@github.com:vector-of-bool/neo-buffer.git ref=develop |
| Name: deps-test | |||||
| Version: 0.0.0 |
| from tests import DDS | |||||
| from tests.fileutil import ensure_dir | |||||
| def test_empty_dir(dds: DDS): | |||||
| with ensure_dir(dds.source_root): | |||||
| dds.build() |
| from contextlib import contextmanager, ExitStack | |||||
| from pathlib import Path | |||||
| from typing import Iterator, Union, Optional | |||||
| import shutil | |||||
| @contextmanager | |||||
| def ensure_dir(dirpath: Path) -> Iterator[Path]: | |||||
| """ | |||||
| Ensure that the given directory (and any parents) exist. When the context | |||||
| exists, removes any directories that were created. | |||||
| """ | |||||
| dirpath = dirpath.absolute() | |||||
| if dirpath.exists(): | |||||
| assert dirpath.is_dir(), f'Directory {dirpath} is a non-directory file' | |||||
| yield dirpath | |||||
| return | |||||
| # Create the directory and clean it up when we are done | |||||
| with ensure_dir(dirpath.parent): | |||||
| dirpath.mkdir() | |||||
| try: | |||||
| yield dirpath | |||||
| finally: | |||||
| shutil.rmtree(dirpath) | |||||
| @contextmanager | |||||
| def auto_delete(fpath: Path) -> Iterator[Path]: | |||||
| try: | |||||
| yield fpath | |||||
| finally: | |||||
| if fpath.exists(): | |||||
| fpath.unlink() | |||||
| @contextmanager | |||||
| def set_contents(fpath: Path, content: bytes) -> Iterator[Path]: | |||||
| prev_content: Optional[bytes] = None | |||||
| if fpath.exists(): | |||||
| assert fpath.is_file(), 'File {fpath} exists and is not a regular file' | |||||
| prev_content = fpath.read_bytes() | |||||
| with ensure_dir(fpath.parent): | |||||
| fpath.write_bytes(content) | |||||
| try: | |||||
| yield fpath | |||||
| finally: | |||||
| if prev_content is None: | |||||
| fpath.unlink() | |||||
| else: | |||||
| fpath.write_bytes(prev_content) |