Skip to content

Commit

Permalink
Improve source layout test discovery (#247)
Browse files Browse the repository at this point in the history
- When `./tests` is present, try to test that instead of packagename
- If src layout is used, there's no need to switch away from the source directory when testing
- No longer use import-mode=importlib, which meant that packages could not do
  relative imports in tests
  • Loading branch information
stefanv committed Nov 12, 2024
2 parents eb1c607 + 5c5266c commit 2bec334
Show file tree
Hide file tree
Showing 21 changed files with 211 additions and 48 deletions.
4 changes: 4 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ repos:
rev: 6f546f30c2b142ad5b3edcf20e3d27cf1789b932 # frozen: v1.10.1
hooks:
- id: mypy
exclude: |
(?x)(
^example_pkg_src/
)
- repo: https://github.com/codespell-project/codespell
rev: "193cd7d27cd571f79358af09a8fb8997e54f8fff" # frozen: v2.3.0
Expand Down
1 change: 1 addition & 0 deletions example_pkg/example_pkg/_core.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
def echo(str) -> None: ...
2 changes: 2 additions & 0 deletions example_pkg_src/.gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.* export-ignore
.spin -export-ignore
5 changes: 5 additions & 0 deletions example_pkg_src/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
build
build-install
.mesonpy-native-file.ini
dist/
doc/_build
20 changes: 20 additions & 0 deletions example_pkg_src/meson.build
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
project(
'spin-example-pkg',
'c',
version: '0.0.dev0',
license: 'BSD-3',
meson_version: '>= 0.64',
default_options: [
'buildtype=debugoptimized',
'c_std=c99',
'cpp_std=c++14',
],
)

cc = meson.get_compiler('c')

py_mod = import('python')
py = py_mod.find_installation(pure: false)
py_dep = py.dependency()

subdir('src')
20 changes: 20 additions & 0 deletions example_pkg_src/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[project]
name = "example_pkg"
version = "0.0dev0"
requires-python = ">=3.7"
description = "spin Example Package"

[build-system]
build-backend = "mesonpy"
requires = [
"meson-python>=0.13.0rc0",
]

[tool.spin]
package = 'example_pkg'

[tool.spin.commands]
"Build" = [
"spin.cmds.meson.build",
"spin.cmds.meson.test"
]
4 changes: 4 additions & 0 deletions example_pkg_src/src/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from ._core import echo

__all__ = ["echo"]
__version__ = "0.0.0dev0"
1 change: 1 addition & 0 deletions example_pkg_src/src/_core.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
def echo(str) -> None: ...
Empty file added example_pkg_src/src/conftest.py
Empty file.
44 changes: 44 additions & 0 deletions example_pkg_src/src/coremodule.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#define PY_SSIZE_T_CLEAN
#include <Python.h>

static PyObject *
core_echo(PyObject *self, PyObject *args)
{
const char *str;
PyObject *ret;

if (!PyArg_ParseTuple(args, "s", &str))
return NULL;

printf("%s\n", str);

ret = PyLong_FromLong(42);
Py_INCREF(ret);
return ret;
}

static PyMethodDef CoreMethods[] = {
{"echo", core_echo, METH_VARARGS, "Echo a string and return 42"},
{NULL, NULL, 0, NULL} /* Sentinel */
};

static struct PyModuleDef coremodule = {
PyModuleDef_HEAD_INIT,
"core", /* name of module */
NULL, /* module documentation, may be NULL */
-1, /* size of per-interpreter state of the module,
or -1 if the module keeps state in global variables. */
CoreMethods
};

PyMODINIT_FUNC
PyInit__core(void)
{
PyObject *m;

m = PyModule_Create(&coremodule);
if (m == NULL)
return NULL;

return m;
}
18 changes: 18 additions & 0 deletions example_pkg_src/src/meson.build
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
py.extension_module(
'_core',
'coremodule.c',
install: true,
subdir: 'example_pkg'
)

python_sources = [
'__init__.py',
'conftest.py'
]

py.install_sources(
python_sources,
subdir: 'example_pkg'
)

install_subdir('submodule', install_dir: py.get_install_dir() / 'example_pkg')
Empty file.
2 changes: 2 additions & 0 deletions example_pkg_src/tests/submodule/test_submodule.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
def test_something():
pass
6 changes: 6 additions & 0 deletions example_pkg_src/tests/test_core.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from example_pkg import echo # type: ignore[attr-defined]


def test_core():
ans = echo("hello world")
assert ans == 42
18 changes: 11 additions & 7 deletions spin/cmds/meson.py
Original file line number Diff line number Diff line change
Expand Up @@ -499,12 +499,16 @@ def test(
raise SystemExit(1)

# User did not specify what to test, so we test
# the full package
# the full package, or the tests directory if that is present
if not (pytest_args or tests):
pytest_args = ("--pyargs", package)
if os.path.isdir("./tests"):
# tests dir exists, presuming you are not shipping tests
# with your package, and prefer to run those instead
pytest_args = (os.path.abspath("./tests"),)
else:
pytest_args = ("--pyargs", package)
elif tests:
if (os.path.sep in tests) or ("/" in tests):
# Tests specified as file path
pytest_args = pytest_args + (tests,)
else:
# Otherwise tests given as modules
Expand Down Expand Up @@ -549,9 +553,6 @@ def test(
if (n_jobs != "1") and ("-n" not in pytest_args):
pytest_args = ("-n", str(n_jobs)) + pytest_args

if not any("--import-mode" in arg for arg in pytest_args):
pytest_args = ("--import-mode=importlib",) + pytest_args

if verbose:
pytest_args = ("-v",) + pytest_args

Expand All @@ -577,8 +578,11 @@ def test(
if not os.path.exists(install_dir):
os.mkdir(install_dir)

# Unless we have a src layout, we need to switch away from the current directory into build install to avoid importing ./package instead of the built package.
test_path = site_path if not os.path.isdir("./src") else None

cwd = os.getcwd()
pytest_p = _run(cmd + list(pytest_args), cwd=site_path)
pytest_p = _run(cmd + list(pytest_args), cwd=test_path)
os.chdir(cwd)

if gcov:
Expand Down
Empty file added spin/tests/__init__.py
Empty file.
17 changes: 13 additions & 4 deletions spin/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,30 @@
from spin import util


@pytest.fixture(autouse=True)
def pre_post_test():
def dir_switcher(path):
# Pre-test code
cwd = os.getcwd()
os.chdir("example_pkg")
os.chdir(path)

try:
yield
finally:
# Post test code
os.chdir(cwd)
util.run(["git", "clean", "-xdf"], cwd="example_pkg")
util.run(["git", "clean", "-xdf"], cwd=path)
os.chdir(cwd)


@pytest.fixture()
def example_pkg():
yield from dir_switcher("example_pkg")


@pytest.fixture()
def example_pkg_src_layout():
yield from dir_switcher("example_pkg_src")


@pytest.fixture
def editable_install():
util.run(["pip", "install", "--quiet", "--no-build-isolation", "-e", "."])
Expand Down
37 changes: 19 additions & 18 deletions spin/tests/test_build_cmds.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@
from pathlib import Path

import pytest
from testutil import (

from spin.cmds.util import run

from .testutil import (
skip_on_windows,
skip_py_lt_311,
skip_unless_linux,
Expand All @@ -14,10 +17,8 @@
stdout,
)

from spin.cmds.util import run


def test_basic_build():
def test_basic_build(example_pkg):
"""Does the package build?"""
spin("build")

Expand All @@ -27,21 +28,21 @@ def test_basic_build():
).exists(), "`build-install` folder not created after `spin build`"


def test_debug_builds():
def test_debug_builds(example_pkg):
"""Does spin generate gcov debug output files?"""
spin("build", "--gcov")

debug_files = Path(".").rglob("*.gcno")
assert len(list(debug_files)) != 0, "debug files not generated for gcov build"


def test_prefix_builds():
def test_prefix_builds(example_pkg):
"""does spin build --prefix create a build-install directory with the correct structure?"""
spin("build", "--prefix=/foobar/")
assert (Path("build-install") / Path("foobar")).exists()


def test_coverage_builds():
def test_coverage_builds(example_pkg):
"""Does gcov test generate coverage files?"""
spin("test", "--gcov")

Expand All @@ -58,7 +59,7 @@ def test_coverage_builds():
("sonarqube", Path("sonarqube.xml")),
],
)
def test_coverage_reports(report_type, output_file):
def test_coverage_reports(example_pkg, report_type, output_file):
"""Does gcov test generate coverage reports?"""
spin("test", "--gcov", f"--gcov-format={report_type}")

Expand All @@ -68,15 +69,15 @@ def test_coverage_reports(report_type, output_file):
), f"coverage report not generated for gcov build ({report_type})"


def test_expand_pythonpath():
def test_expand_pythonpath(example_pkg):
"""Does an $ENV_VAR get expanded in `spin run`?"""
output = spin("run", "echo $PYTHONPATH")
assert any(
p in stdout(output) for p in ("site-packages", "dist-packages")
), f"Expected value of $PYTHONPATH, got {stdout(output)} instead"


def test_run_stdout():
def test_run_stdout(example_pkg):
"""Ensure `spin run` only includes command output on stdout."""
p = spin(
"run",
Expand All @@ -92,7 +93,7 @@ def test_run_stdout():
# Detecting whether a file is executable is not that easy on Windows,
# as it seems to take into consideration whether that file is associated as an executable.
@skip_on_windows
def test_recommend_run_python():
def test_recommend_run_python(example_pkg):
"""If `spin run file.py` is called, is `spin run python file.py` recommended?"""
with tempfile.NamedTemporaryFile(suffix=".py") as f:
p = spin("run", f.name, sys_exit=False)
Expand All @@ -101,20 +102,20 @@ def test_recommend_run_python():
), "Failed to recommend `python run python file.py`"


def test_sdist():
def test_sdist(example_pkg):
spin("sdist")


def test_example():
def test_example(example_pkg):
spin("example")


def test_docs():
def test_docs(example_pkg):
run(["pip", "install", "--quiet", "sphinx"])
spin("docs")


def test_spin_install():
def test_spin_install(example_pkg):
cwd = os.getcwd()
spin("install")
with tempfile.TemporaryDirectory() as d:
Expand All @@ -135,7 +136,7 @@ def test_spin_install():


@skip_unless_linux
def test_gdb():
def test_gdb(example_pkg):
p = spin(
"gdb",
"-c",
Expand All @@ -149,7 +150,7 @@ def test_gdb():


@skip_unless_macos
def test_lldb():
def test_lldb(example_pkg):
p = spin(
"lldb",
"-c",
Expand All @@ -163,7 +164,7 @@ def test_lldb():


@skip_py_lt_311 # python command does not run on older pythons
def test_parallel_builds():
def test_parallel_builds(example_pkg):
spin("build")
spin("build", "-C", "parallel/build")
p = spin("python", "--", "-c", "import example_pkg; print(example_pkg.__file__)")
Expand Down
8 changes: 4 additions & 4 deletions spin/tests/test_cli.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
from testutil import spin, stdout

import spin as libspin

from .testutil import spin, stdout


def test_get_version():
def test_get_version(example_pkg):
p = spin("--version")
assert stdout(p) == f"spin {libspin.__version__}"


def test_arg_override():
def test_arg_override(example_pkg):
p = spin("example")
assert "--test is: default override" in stdout(p)
assert "Default kwd is: 3" in stdout(p)
Expand Down
6 changes: 3 additions & 3 deletions spin/tests/test_editable.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
from testutil import spin, stdout
from .testutil import spin, stdout


def test_detect_editable(editable_install):
def test_detect_editable(example_pkg, editable_install):
assert "Editable install of same source detected" in stdout(
spin("build")
), "Failed to detect and warn about editable install"


def test_editable_tests(editable_install):
def test_editable_tests(example_pkg, editable_install):
spin("test")
Loading

0 comments on commit 2bec334

Please sign in to comment.