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