This page documents the pytest-based test suite for YORU, taking into account the project’s pytest.ini
defaults. It explains how to run the tests, what they cover, and how to extend them for your workflow and CI.
pytest.ini
The repository configures pytest like this:
[pytest]
addopts = -vv -rA
testpaths = tests
markers =
gui: tests that require a GUI window
hw: tests that require hardware (camera, Arduino, etc.)
slow: slow or heavy tests (training/inference)
-vv -rA
are applied by default; you’ll see verbose output and reasons for all outcomes (passes, failures, errors, skips, xfails).tests/
by default.gui
, hw
, and slow
are standard across the suite.You can still override or add flags at the command line (e.g.,
-q
for quiet,--maxfail=1
, etc.).
We organize checks into four practical “layers” to catch problems early while keeping the default run fast:
config/template.yaml
exist.jsonschema
is installed.python -m yoru --help
exits successfully and quickly (no heavy imports at top-level CLI).trigger_plugins/*.py
is importable without real hardware.pyserial
(serial.tools.list_ports
), libs.arduino
, and NI‑DAQ (nidaqmx.constants
, nidaqmx.errors.DaqError
) so imports succeed.@slow
)
.pt
to assert “no crash”.These layers together protect: config → startup → inference path → training path.
Because pytest.ini
already enables -vv -rA
, you can keep the CLI short.
Exclude gui
and hw
by default; also exclude slow
unless you explicitly want the smokes:
pytest -m "not gui and not hw and not slow"
pytest -q -m "not gui and not hw and not slow"
(Already included by default via -rA
, but you can combine with selection as needed)
pytest -m "not gui and not hw" -rA
pytest -m "not gui and not hw" --durations=10
print()
output live (disable capture)pytest -m "not gui and not hw" -s
A compact progress line is available via a small plugin shipped under tests/conftest.py
.
pytest --progress=on
pytest --progress=off
The line shows a live bar and counts (✓ passed / ✗ failed / ~ skipped).
@slow
)These are off by default; enable on demand by selecting the marks and by providing small test assets.
Pillow
(pip install pillow
).pt
file (set as env var)Run:
export YORU_SMOKE_WEIGHTS=weights/tiny.pt # path to a very small model file
pytest -k inference_smoke -m "slow and not gui and not hw"
Run:
export YORU_SMOKE_DATA=/path/to/small_dataset
pytest -k training_smoke -m "slow and not gui and not hw"
Both smokes can optionally call a CLI if a small Python API isn’t available. Provide templates via:
YORU_SMOKE_CMD
— e.g. python -m yoru infer --input {images} --weights {weights} --out {out}
YORU_TRAIN_CMD
— e.g. python -m yoru train --data {data} --epochs {epochs} --out {out} --device cpu
The tests will prefer yoru.testing.run_inference(...)
/ yoru.testing.run_training(...)
if present; otherwise they expand the above templates and run a subprocess.
tests/test_repo_layout.py
— Basic repository shape and presence checks for required files.tests/test_imports.py
— import yoru
and (optionally) import yoru.__main__
must succeed.tests/test_cli_help.py
(@gui
) — python -m yoru --help
returns 0 (fast, no top-level heavies).tests/test_config_template.py
— Validates the template YAML’s top‑level keys and nested sections.tests/test_config_schema.py
— Strict JSON‑Schema validation (skips if jsonschema
is not installed).tests/test_trigger_plugins_import.py
— Discovers trigger_plugins/*.py
and asserts they are importable under hardware stubs.tests/test_inference_smoke.py
(@slow
) — “does not crash” inference with dummy images and a tiny model.tests/test_training_smoke.py
(@slow
) — “writes artifacts” training on CPU for 1 epoch.trigger_plugins/my_plugin.py
.tests/conftest.py
:
# inside fake_serial() in conftest.py
import types, sys
myhw = types.ModuleType("myhw"); myhw.__path__ = [] # package if submodules are used
sys.modules["myhw"] = myhw # and any myhw.submodule you need
yoru/cli.py
(keep imports lazy).def test_newcmd_help():
proc = subprocess.run([sys.executable, "-m", "yoru", "newcmd", "--help"], check=False, capture_output=True, text=True)
assert proc.returncode == 0
assert "New command description" in proc.stdout
tests/schema/yoru_config.schema.json
) that matches the public template.anyOf
when introducing new names/fields to keep backward compatibility.Provide thin wrappers so both CLI and tests can reuse them:
# yoru/testing.py
def run_inference(images_dir: str, weights_path: str, out_dir: str) -> None: ...
def run_training(data_dir: str, out_dir: str, epochs: int = 1, device: str = "cpu") -> None: ...
The smoke tests auto-detect these functions; otherwise they fall back to the CLI templates.
Example (GitHub Actions):
name: tests
on: [push, pull_request]
jobs:
fast:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: '3.10' }
- run: pip install -U pip pytest pyyaml jsonschema pillow
- run: pytest -m "not gui and not hw and not slow"
smoke:
if: github.event_name == 'schedule' || github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: '3.10' }
- run: pip install -U pip pytest pyyaml pillow
- run: |
echo "No tiny weights/data provided on CI → skip smokes by design"
pytest -k "inference_smoke or training_smoke" -m "slow and not gui and not hw" || true
UnicodeDecodeError when reading YAML
Hidden characters (BOM/NBSP/zero‑width space) can corrupt YAML. Clean the file or sanitize before parsing.
ModuleNotFoundError for serial
, nidaqmx
, libs.arduino
during plugin imports
The tests inject stubs under tests/conftest.py
. If your plugin imports new modules (nidaqmx.something
, custom myhw
), add a minimal stub there.
CLI help test fails
Keep heavy imports out of the top of yoru/cli.py
. Import frameworks lazily inside subcommands.
pytest -m "not gui and not hw and not slow"
for the fast layer; markers come from pytest.ini
.@slow
tests when you need end‑to‑end checks.