Переглянути джерело

New smoke tests based on pytest

default_compile_flags
vector-of-bool 5 роки тому
джерело
коміт
62b60c2dfe
15 змінених файлів з 427 додано та 0 видалено
  1. +1
    -0
      tests/__init__.py
  2. +14
    -0
      tests/basics/test_app_only.py
  3. +44
    -0
      tests/basics/test_basics.py
  4. +30
    -0
      tests/basics/test_simple.py
  5. +14
    -0
      tests/basics/test_test_only.py
  6. +38
    -0
      tests/conftest.py
  7. +195
    -0
      tests/dds.py
  8. +25
    -0
      tests/deps/do_test.py
  9. +4
    -0
      tests/deps/git-remote/package.dds
  10. +1
    -0
      tests/deps/git-remote/remote.dds
  11. +2
    -0
      tests/deps/no-deps/package.dds
  12. +0
    -0
      tests/deps/no-deps/remote.dds
  13. +7
    -0
      tests/errors/errors_test.py
  14. +52
    -0
      tests/fileutil.py
  15. +0
    -0
      tests/proc.py

+ 1
- 0
tests/__init__.py Переглянути файл

@@ -0,0 +1 @@
from .dds import DDS, DDSFixtureParams, scoped_dds, dds_fixture_conf, dds_fixture_conf_1

+ 14
- 0
tests/basics/test_app_only.py Переглянути файл

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

+ 44
- 0
tests/basics/test_basics.py Переглянути файл

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

+ 30
- 0
tests/basics/test_simple.py Переглянути файл

@@ -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*')) != []

+ 14
- 0
tests/basics/test_test_only.py Переглянути файл

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

+ 38
- 0
tests/conftest.py Переглянути файл

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

+ 195
- 0
tests/dds.py Переглянути файл

@@ -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=['.'])

+ 25
- 0
tests/deps/do_test.py Переглянути файл

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

+ 4
- 0
tests/deps/git-remote/package.dds Переглянути файл

@@ -0,0 +1,4 @@
Name: deps-test
Version: 0.0.0

Depends: neo-buffer 0.1.0

+ 1
- 0
tests/deps/git-remote/remote.dds Переглянути файл

@@ -0,0 +1 @@
Remote-Package: neo-buffer 0.1.0; git url=git@github.com:vector-of-bool/neo-buffer.git ref=develop

+ 2
- 0
tests/deps/no-deps/package.dds Переглянути файл

@@ -0,0 +1,2 @@
Name: deps-test
Version: 0.0.0

+ 0
- 0
tests/deps/no-deps/remote.dds Переглянути файл


+ 7
- 0
tests/errors/errors_test.py Переглянути файл

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

+ 52
- 0
tests/fileutil.py Переглянути файл

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

+ 0
- 0
tests/proc.py Переглянути файл


Завантаження…
Відмінити
Зберегти