| @@ -0,0 +1 @@ | |||
| from .dds import DDS, DDSFixtureParams, scoped_dds, dds_fixture_conf, dds_fixture_conf_1 | |||
| @@ -0,0 +1,14 @@ | |||
| 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() | |||
| @@ -0,0 +1,44 @@ | |||
| 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() | |||
| @@ -0,0 +1,30 @@ | |||
| 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*')) != [] | |||
| @@ -0,0 +1,14 @@ | |||
| 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() | |||
| @@ -0,0 +1,38 @@ | |||
| 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 | |||
| @@ -0,0 +1,195 @@ | |||
| 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=['.']) | |||
| @@ -0,0 +1,25 @@ | |||
| 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' | |||
| @@ -0,0 +1,4 @@ | |||
| Name: deps-test | |||
| Version: 0.0.0 | |||
| Depends: neo-buffer 0.1.0 | |||
| @@ -0,0 +1 @@ | |||
| Remote-Package: neo-buffer 0.1.0; git url=git@github.com:vector-of-bool/neo-buffer.git ref=develop | |||
| @@ -0,0 +1,2 @@ | |||
| Name: deps-test | |||
| Version: 0.0.0 | |||
| @@ -0,0 +1,7 @@ | |||
| from tests import DDS | |||
| from tests.fileutil import ensure_dir | |||
| def test_empty_dir(dds: DDS): | |||
| with ensure_dir(dds.source_root): | |||
| dds.build() | |||
| @@ -0,0 +1,52 @@ | |||
| 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) | |||