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) |