From 2feab9f15aa2844af8b94821ce8ca15d3840c0b0 Mon Sep 17 00:00:00 2001 From: Matt Drozt Date: Wed, 12 Aug 2026 21:09:26 -0500 Subject: [PATCH 01/16] Adds three of the examples to the test suite, checking that return code and stdout match what is expected. --- .../active-learn/cpp-mpi-with-ddict/driver.py | 2 +- example/cpp-exchange/dragon/driver.py | 10 +- example/cpp-exchange/dragon/requirements.txt | 1 + example/cpp-exchange/dragon/result.txt | 23 +++ example/cpp-exchange/in-mem/driver.py | 2 +- example/cpp-exchange/in-mem/result.txt | 11 ++ example/py-cpp-exchange/dragon/app.cpp | 9 +- example/py-cpp-exchange/dragon/driver.py | 60 +++--- .../py-cpp-exchange/dragon/requirements.txt | 1 + example/py-cpp-exchange/dragon/result.txt | 59 ++++++ tests/examples/conftest.py | 177 ++++++++++++++++++ tests/examples/test_examples.py | 7 + 12 files changed, 330 insertions(+), 32 deletions(-) create mode 100644 example/cpp-exchange/dragon/requirements.txt create mode 100644 example/cpp-exchange/dragon/result.txt create mode 100644 example/cpp-exchange/in-mem/result.txt create mode 100644 example/py-cpp-exchange/dragon/requirements.txt create mode 100644 example/py-cpp-exchange/dragon/result.txt create mode 100644 tests/examples/conftest.py create mode 100644 tests/examples/test_examples.py diff --git a/example/active-learn/cpp-mpi-with-ddict/driver.py b/example/active-learn/cpp-mpi-with-ddict/driver.py index ed9d7ab..c716485 100644 --- a/example/active-learn/cpp-mpi-with-ddict/driver.py +++ b/example/active-learn/cpp-mpi-with-ddict/driver.py @@ -65,7 +65,7 @@ MAX_ITER: int = 10 # hard cap on iterations # ── Consts ───────────────────────────────────────────────────────────────────── -HERE = Path(__file__).parent.absolute() +HERE = Path(__file__).resolve().parent ROOT = HERE.parent.parent.parent EXAMPLES_BIN_DIR = ROOT / "install" / "bin" / "examples" diff --git a/example/cpp-exchange/dragon/driver.py b/example/cpp-exchange/dragon/driver.py index 802e8b1..1ca6ff4 100644 --- a/example/cpp-exchange/dragon/driver.py +++ b/example/cpp-exchange/dragon/driver.py @@ -5,13 +5,19 @@ from dragon.data.ddict import DDict from dragon.native.process import Process, ProcessTemplate -HERE = pathlib.Path(__file__).parent.absolute() +HERE = pathlib.Path(__file__).resolve().parent ROOT = HERE.parent.parent.parent EXAMPLES_BIN_DIR = ROOT / "install" / "bin" / "examples" def main() -> int: - dd = DDict(managers_per_node=1, n_nodes=1, trace=False) + dd = DDict( + managers_per_node=1, + n_nodes=1, + trace=False, + wait_for_keys=True, + working_set_size=3, + ) serial_dd = dd.serialize() producer_tmpl = ProcessTemplate( target=os.fspath(EXAMPLES_BIN_DIR / "dragon-cpp-producer"), diff --git a/example/cpp-exchange/dragon/requirements.txt b/example/cpp-exchange/dragon/requirements.txt new file mode 100644 index 0000000..547ce39 --- /dev/null +++ b/example/cpp-exchange/dragon/requirements.txt @@ -0,0 +1 @@ +dragonhpc[telemetry]>=0.14.1 diff --git a/example/cpp-exchange/dragon/result.txt b/example/cpp-exchange/dragon/result.txt new file mode 100644 index 0000000..77127c4 --- /dev/null +++ b/example/cpp-exchange/dragon/result.txt @@ -0,0 +1,23 @@ +==> Running Producer... +=========================== +Hello World from Producer!! +--------------------------- +--------------------------- +Goodbye from Producer +=========================== +==> Producer Joined +==> Running Consumer... +=========================== +Hello World from Consumer!! +--------------------------- +Some Int: 123 +Some Float: 1.23 +Some Int Tensor: [ 1, 2, 3, 4, 5, 6, 7, 8, ] + \ -> Dims: [ 2, 4, ] +Some Float Tensor: [ 0.120000, 3.450000, 6.780000, 9.123000, ] + \ -> Dims: [ 4, ] +--------------------------- +Goodbye from Consumer!! +=========================== +==> Consumer Joined ++++ head proc exited, code 0 diff --git a/example/cpp-exchange/in-mem/driver.py b/example/cpp-exchange/in-mem/driver.py index 87db0e2..b75df34 100644 --- a/example/cpp-exchange/in-mem/driver.py +++ b/example/cpp-exchange/in-mem/driver.py @@ -1,7 +1,7 @@ import pathlib import subprocess -HERE = pathlib.Path(__file__).parent.absolute() +HERE = pathlib.Path(__file__).resolve().parent ROOT = HERE.parent.parent.parent EXAMPLES_BIN_DIR = ROOT / "install" / "bin" / "examples" diff --git a/example/cpp-exchange/in-mem/result.txt b/example/cpp-exchange/in-mem/result.txt new file mode 100644 index 0000000..35e2012 --- /dev/null +++ b/example/cpp-exchange/in-mem/result.txt @@ -0,0 +1,11 @@ +============= +Hello World!! +============= +Some Int: 123 +Some Double: 1.23 + +Some Int Tensor: [ 0, 1, 2, 3, ] + \- Dims: [ 2, 2, ] +Some Double Tensor: [ 0.100000, 2.300000, 4.500000, 6.700000, ] + \- Dims: [ 4, ] +============= diff --git a/example/py-cpp-exchange/dragon/app.cpp b/example/py-cpp-exchange/dragon/app.cpp index 880b14f..5157625 100644 --- a/example/py-cpp-exchange/dragon/app.cpp +++ b/example/py-cpp-exchange/dragon/app.cpp @@ -43,6 +43,7 @@ void print_vector_key(radex::IClient &client, const std::string &key) { } int main() { + const auto DELAY_SET_TIME = 1'000ms; char *serialized_dd = getenv("SERIALIZED_DDICT"); if (serialized_dd == nullptr) { throw std::runtime_error("DDict descriptor not found!"); @@ -59,22 +60,22 @@ int main() { print_vector_key(client, "py-int-tensor"); print_vector_key(client, "py-float-tensor"); - std::this_thread::sleep_for(3'000ms); + std::this_thread::sleep_for(DELAY_SET_TIME); std::cout << IDENT << "App: Setting Double" << std::endl; client.put_scalar(radex::data::OutgoingHandle{"cpp-double"}, 1.23); - std::this_thread::sleep_for(3'000ms); + std::this_thread::sleep_for(DELAY_SET_TIME); std::cout << IDENT << "App: Setting Int" << std::endl; client.put_scalar(radex::data::OutgoingHandle{"cpp-int"}, 987); - std::this_thread::sleep_for(3'000ms); + std::this_thread::sleep_for(DELAY_SET_TIME); std::cout << IDENT << "App: Setting Double Tensor" << std::endl; std::vector v(12); std::iota(v.begin(), v.end(), 0); client.put_tensor(radex::data::OutgoingHandle{"cpp-double-tensor"}, {4, 3}, v); - std::this_thread::sleep_for(3'000ms); + std::this_thread::sleep_for(DELAY_SET_TIME); std::cout << IDENT << "App: Setting Long Tensor" << std::endl; client.put_tensor(radex::data::OutgoingHandle{"cpp-long-tensor"}, {2, 2, 2}, {1, 2, 3, 4, 5, 6, 7, 8}); diff --git a/example/py-cpp-exchange/dragon/driver.py b/example/py-cpp-exchange/dragon/driver.py index 3a425f4..9586221 100644 --- a/example/py-cpp-exchange/dragon/driver.py +++ b/example/py-cpp-exchange/dragon/driver.py @@ -8,17 +8,23 @@ import numpy as np from dragon.data.ddict import DDict from dragon.native.process import Process, ProcessTemplate - from radex.clients.core import DragonClient as Client from radex.handles.handles import IncomingHandle, OutgoingHandle -HERE = pathlib.Path(__file__).parent.absolute() +HERE = pathlib.Path(__file__).resolve().parent ROOT = HERE.parent.parent.parent EXAMPLES_BIN_DIR = ROOT / "install" / "bin" / "examples" +DELAY_SET_TIME = 1 def main() -> int: - dd = DDict(managers_per_node=1, n_nodes=1, trace=False) + dd = DDict( + managers_per_node=1, + n_nodes=1, + trace=False, + wait_for_keys=True, + working_set_size=3, + ) serial_dd = dd.serialize() app_tmpl = ProcessTemplate( target=os.fspath(EXAMPLES_BIN_DIR / "dragon-cpp-with-py"), @@ -26,36 +32,36 @@ def main() -> int: ) app = Process.from_template(app_tmpl) - print(f"Driver: Making client") + print(f"Driver: Making client", flush=True) client = Client(serial_dd, 5) - print(f"Driver: Starting app") + print(f"Driver: Starting app", flush=True) app.start() try: - time.sleep(3) - print("Driver: Setting Int") + time.sleep(DELAY_SET_TIME) + print("Driver: Setting Int", flush=True) client.put_scalar(OutgoingHandle("py-int"), 123) - time.sleep(3) - print("Driver: Setting Double") + time.sleep(DELAY_SET_TIME) + print("Driver: Setting Double", flush=True) client.put_scalar(OutgoingHandle("py-double"), 9.87) - time.sleep(3) - print("Driver: Setting Numpy Int") + time.sleep(DELAY_SET_TIME) + print("Driver: Setting Numpy Int", flush=True) client.put_scalar(OutgoingHandle("py-np-float"), np.float32(45.6)) - time.sleep(3) - print("Driver: Setting Int Tensor") + time.sleep(DELAY_SET_TIME) + print("Driver: Setting Int Tensor", flush=True) client.put_tensor(OutgoingHandle("py-int-tensor"), np.arange(4, dtype=np.int32)) - time.sleep(3) - print("Driver: Setting Float Tensor") + time.sleep(DELAY_SET_TIME) + print("Driver: Setting Float Tensor", flush=True) client.put_tensor( OutgoingHandle("py-float-tensor"), np.arange(12, dtype=np.float64).reshape((6, 2)), ) - print(f"Driver: Looking for keys") + print(f"Driver: Looking for keys", flush=True) print_scalar(client, "cpp-double") print_scalar(client, "cpp-int") print_tensor(client, "cpp-double-tensor") @@ -65,9 +71,9 @@ def main() -> int: py_obj_key = "my-py-obj" obj = C("spam-and-eggs") client.put_picklable(py_obj_key, obj) - print("Driver: Getting a py object") + print("Driver: Getting a py object", flush=True) recv = client.get_picklable(py_obj_key) - print(f"Driver: Got object `{recv}`") + print(f"Driver: Got object `{recv}`", flush=True) finally: app.join() @@ -75,24 +81,30 @@ def main() -> int: def print_scalar(client, key): - print(f"Driver: Waiting for scalar key `{key}`") + print(f"Driver: Waiting for scalar key `{key}`", flush=True) scalar = client.wait_for_scalar(IncomingHandle(key), 10) - print(textwrap.dedent(f"""\ + print( + textwrap.dedent(f"""\ Driver: Got scalar: |- Type: {scalar.dtype} \\- Value: {scalar} - """)) + """), + flush=True, + ) def print_tensor(client, key): - print(f"Driver: Waiting for tensor key `{key}`") + print(f"Driver: Waiting for tensor key `{key}`", flush=True) tensor = client.wait_for_tensor(IncomingHandle(key), 10) - print(textwrap.dedent(f"""\ + print( + textwrap.dedent(f"""\ Driver: Got tensor: |- Type: {tensor.dtype} |- Dims: {tensor.shape} \\- Data: {tensor.ravel()} - """)) + """), + flush=True, + ) @dataclasses.dataclass(frozen=True) diff --git a/example/py-cpp-exchange/dragon/requirements.txt b/example/py-cpp-exchange/dragon/requirements.txt new file mode 100644 index 0000000..547ce39 --- /dev/null +++ b/example/py-cpp-exchange/dragon/requirements.txt @@ -0,0 +1 @@ +dragonhpc[telemetry]>=0.14.1 diff --git a/example/py-cpp-exchange/dragon/result.txt b/example/py-cpp-exchange/dragon/result.txt new file mode 100644 index 0000000..fbddb24 --- /dev/null +++ b/example/py-cpp-exchange/dragon/result.txt @@ -0,0 +1,59 @@ +Driver: Making client +Driver: Starting app + App: Creating client + App: Client created + App: Waiting for scalar key `py-int` +Driver: Setting Int + App: Got key `py-int` has value 123 + + App: Waiting for scalar key `py-double` +Driver: Setting Double + App: Got key `py-double` has value 9.87 + + App: Waiting for scalar key `py-np-float` +Driver: Setting Numpy Int + App: Got key `py-np-float` has value 45.6 + + App: Waiting for tensor key `py-int-tensor` +Driver: Setting Int Tensor + App: Got key `py-int-tensor` + |- Data: [ 0, 1, 2, 3, ] + \- Dims: [ 4, ] + + App: Waiting for tensor key `py-float-tensor` +Driver: Setting Float Tensor +Driver: Looking for keys +Driver: Waiting for scalar key `cpp-double` + App: Got key `py-float-tensor` + |- Data: [ 0.000000, 1.000000, 2.000000, 3.000000, 4.000000, 5.000000, 6.000000, 7.000000, 8.000000, 9.000000, 10.000000, 11.000000, ] + \- Dims: [ 6, 2, ] + + App: Setting Double +Driver: Got scalar: + |- Type: float64 + \- Value: 1.23 + +Driver: Waiting for scalar key `cpp-int` + App: Setting Int +Driver: Got scalar: + |- Type: int32 + \- Value: 987 + +Driver: Waiting for tensor key `cpp-double-tensor` + App: Setting Double Tensor +Driver: Got tensor: + |- Type: float64 + |- Dims: (4, 3) + \- Data: [ 0. 1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11.] + +Driver: Waiting for tensor key `cpp-long-tensor` + App: Setting Long Tensor +Driver: Got tensor: + |- Type: int32 + |- Dims: (2, 2, 2) + \- Data: [1 2 3 4 5 6 7 8] + +Driver: Setting a py object +Driver: Getting a py object +Driver: Got object `C(msg='spam-and-eggs')` ++++ head proc exited, code 0 diff --git a/tests/examples/conftest.py b/tests/examples/conftest.py new file mode 100644 index 0000000..3ec64fe --- /dev/null +++ b/tests/examples/conftest.py @@ -0,0 +1,177 @@ +from __future__ import annotations + +import abc +import dataclasses +import os +import pathlib +import re +import subprocess as sp +import sys +import importlib.metadata + +import pytest + +HERE = pathlib.Path(__file__).absolute().parent +ROOT = HERE.parent.parent +EXAMPLES_DIR = ROOT / "example" + + +@dataclasses.dataclass(frozen=True) +class Example(abc.ABC): + directory: pathlib.Path + marks: list[pytest.MarkDecorator] = dataclasses.field( + default_factory=list, kw_only=True + ) + + @abc.abstractmethod + def _run(self, cwd, out, err) -> int: ... + + @property + def test_id(self) -> str: + return os.fspath(self.directory.relative_to(EXAMPLES_DIR)) + + @property + def driver(self) -> pathlib.Path: + return self.directory / "driver.py" + + @property + def requirements_file(self) -> pathlib.Path: + return self.directory / "requirements.txt" + + @property + def expected_stdout(self) -> pathlib.Path: + return self.directory / "result.txt" + + def check_for_requirements(self) -> None: + if not self.requirements_file.exists(): + return + pattern = re.compile(r"^\s*([\w\.\-]+)") + with self.requirements_file.open("r", encoding="utf-8") as reqs: + for req in reqs: + match = pattern.match(req) + if match is None: + continue + pkg_name = match.group(1) + try: + importlib.metadata.version(pkg_name) + except importlib.metadata.PackageNotFoundError: + pytest.skip( + f"Failed to find requirement `{pkg_name}`. " + "Try running " + f"`pip install -r {os.fspath(self.requirements_file)}`" + ) + + def run(self, *, where) -> tuple[int, pathlib.Path, pathlib.Path]: + self.check_for_requirements() + out_file = where / "example.out" + err_file = where / "example.err" + with ( + open(out_file, "w", encoding="utf-8") as out, + open(err_file, "w", encoding="utf-8") as err, + ): + return self._run(self.directory, out, err), out_file, err_file + + +class LocalExample(Example): + def _run(self, cwd, out, err) -> int: + return sp.run( + [sys.executable, os.fspath(self.driver)], cwd=cwd, stdout=out, stderr=err + ).returncode + + +class DragonExample(Example): + def __init__( + self, + directory: pathlib.Path, + num_nodes: int | None, + *, + marks: list[pytest.MarkDecorator] | None = None, + ) -> None: + marks = marks or [] + dragon_args = [] + if num_nodes is not None and num_nodes <= 0: + raise ValueError( + "Dragon examples must either be runnable without an allocation " + "or on a posative number of nodes" + ) + + try: + import dragon + from dragon.globalservices.api_setup import get_gs_ret_cuid + from dragon.native.machine import System as DrgSystem + except ImportError: + marks.append(pytest.mark.skip(reason="This example requires dragon")) + else: + NOT_ENOUGH_NODES = pytest.mark.skip( + reason=f"Example requires an allocation of {num_nodes} node(s)" + ) + try: + get_gs_ret_cuid() + except Exception: + # Test suite was not run through dragon + dragon_args.append("-s") + if num_nodes is not None: + marks.append(NOT_ENOUGH_NODES) + else: + # Test suite was run through dragon (including `dragon -s ...`) + drg_system = DrgSystem() + if num_nodes is not None and num_nodes < drg_system.nnodes: + marks.append(NOT_ENOUGH_NODES) + + # FIXME: We should allow for running multinode examples. This + # is a good enough starting point to wire in a few + # single node examples for now. We should probably move + # this over to launching a proper + # `dragon.native.process.Process` or equivalent. + if num_nodes is None: + dragon_args.append("-s") + else: + dragon_args.extend(["-N", str(num_nodes)]) + marks.append( + pytest.mark.xfail( + strict=False, + reason=( + "Examples that cannot be run with `dragon -s ...` " + "are not fully supported in the test suite. See " + "https://github.com/radical-cybertools/radex/issues/25 " + "for more info" + ), + ) + ) + + super().__init__(directory, marks=marks) + self._dragon_args = dragon_args + + def _run(self, cwd, out, err) -> int: + return sp.run( + [ + sys.executable, + "-m", + "dragon", + "--", + *self._dragon_args, + os.fspath(self.driver), + ], + cwd=cwd, + stdout=out, + stderr=err, + ).returncode + + +@pytest.fixture( + scope="function", + params=[ + pytest.param(example, id=example.test_id, marks=example.marks) + for example in [ + LocalExample(EXAMPLES_DIR / "cpp-exchange/in-mem"), + DragonExample(EXAMPLES_DIR / "cpp-exchange/dragon", num_nodes=None), + DragonExample( + EXAMPLES_DIR / "py-cpp-exchange/dragon", + num_nodes=None, + marks=[pytest.mark.slow], + ), + ] + ], +) +def example(request): + yield request.param diff --git a/tests/examples/test_examples.py b/tests/examples/test_examples.py new file mode 100644 index 0000000..c9500f6 --- /dev/null +++ b/tests/examples/test_examples.py @@ -0,0 +1,7 @@ +import filecmp + + +def test_run_example(example, tmp_path): + returncode, out, err = example.run(where=tmp_path) + assert returncode == 0 + assert filecmp.cmp(out, example.expected_stdout) From 99817a0fe5138165f98b89f049eddf1b05d2ff48 Mon Sep 17 00:00:00 2001 From: Matt Drozt Date: Tue, 18 Aug 2026 13:47:05 -0500 Subject: [PATCH 02/16] Style --- tests/examples/conftest.py | 2 +- tests/python/dragon/conftest.py | 1 - tests/python/dragon/test_client_init.py | 3 +-- tests/python/dragon/test_put_and_get_item.py | 1 - tests/python/dragon/test_py_cpp_interop.py | 1 - tests/python/dragon/test_wait_for_item.py | 1 - 6 files changed, 2 insertions(+), 7 deletions(-) diff --git a/tests/examples/conftest.py b/tests/examples/conftest.py index 3ec64fe..b522ede 100644 --- a/tests/examples/conftest.py +++ b/tests/examples/conftest.py @@ -2,12 +2,12 @@ import abc import dataclasses +import importlib.metadata import os import pathlib import re import subprocess as sp import sys -import importlib.metadata import pytest diff --git a/tests/python/dragon/conftest.py b/tests/python/dragon/conftest.py index adf2921..54c330d 100644 --- a/tests/python/dragon/conftest.py +++ b/tests/python/dragon/conftest.py @@ -5,7 +5,6 @@ from dragon.data.ddict import DDict from dragon.globalservices.api_setup import get_gs_ret_cuid from dragon.infrastructure.facts import DRAGON_BASE_DIR - from radex.clients.core import DragonClient as Client diff --git a/tests/python/dragon/test_client_init.py b/tests/python/dragon/test_client_init.py index 9f08601..e9a20dd 100644 --- a/tests/python/dragon/test_client_init.py +++ b/tests/python/dragon/test_client_init.py @@ -1,9 +1,8 @@ import os import pytest -from dragon.data.ddict import DDict - import radex.clients.core +from dragon.data.ddict import DDict from radex.handles.handles import IncomingHandle, OutgoingHandle diff --git a/tests/python/dragon/test_put_and_get_item.py b/tests/python/dragon/test_put_and_get_item.py index 1ccc030..5692318 100644 --- a/tests/python/dragon/test_put_and_get_item.py +++ b/tests/python/dragon/test_put_and_get_item.py @@ -2,7 +2,6 @@ import numpy as np import pytest - from radex.handles.handles import IncomingHandle, OutgoingHandle diff --git a/tests/python/dragon/test_py_cpp_interop.py b/tests/python/dragon/test_py_cpp_interop.py index c098f54..75f3880 100644 --- a/tests/python/dragon/test_py_cpp_interop.py +++ b/tests/python/dragon/test_py_cpp_interop.py @@ -7,7 +7,6 @@ import numpy as np import pytest from dragon.native.process import Popen - from radex.handles.handles import IncomingHandle, OutgoingHandle diff --git a/tests/python/dragon/test_wait_for_item.py b/tests/python/dragon/test_wait_for_item.py index 1594321..6872687 100644 --- a/tests/python/dragon/test_wait_for_item.py +++ b/tests/python/dragon/test_wait_for_item.py @@ -4,7 +4,6 @@ import time import pytest - from radex.clients.core import DragonClient from radex.handles.handles import IncomingHandle, OutgoingHandle From dca6b8987bdcdfc62ca17f9e0c0fecd6a28be6e8 Mon Sep 17 00:00:00 2001 From: Matt Drozt Date: Tue, 18 Aug 2026 13:55:23 -0500 Subject: [PATCH 03/16] Add example test mark --- dev-resources/radex-config.toml | 5 +++-- tests/examples/conftest.py | 3 +++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/dev-resources/radex-config.toml b/dev-resources/radex-config.toml index 9239999..d41daa8 100644 --- a/dev-resources/radex-config.toml +++ b/dev-resources/radex-config.toml @@ -6,7 +6,8 @@ strict_config = true strict_markers = true markers = [ "slow: Test may be slow to run", - "compiled: Test has a component compiled at run time" + "compiled: Test has a component compiled at run time", + "example: Test is running one of the examples", ] [tool.black] @@ -15,4 +16,4 @@ markers = [ profile = "black" py_version = 312 src_paths = ["../src/python/src", "../tests", "../example"] -known_first_party = ["radex"] \ No newline at end of file +known_first_party = ["radex"] diff --git a/tests/examples/conftest.py b/tests/examples/conftest.py index b522ede..cadb56e 100644 --- a/tests/examples/conftest.py +++ b/tests/examples/conftest.py @@ -23,6 +23,9 @@ class Example(abc.ABC): default_factory=list, kw_only=True ) + def __post_init__(self): + self.marks.append(pytest.mark.example) + @abc.abstractmethod def _run(self, cwd, out, err) -> int: ... From 092b7fba3c2bee2c0e614c08c9a1f438704462dd Mon Sep 17 00:00:00 2001 From: Matt Drozt Date: Tue, 18 Aug 2026 14:00:48 -0500 Subject: [PATCH 04/16] Re-style according to dev-resources config --- example/py-cpp-exchange/dragon/driver.py | 1 + tests/python/dragon/conftest.py | 1 + tests/python/dragon/test_client_init.py | 3 ++- tests/python/dragon/test_put_and_get_item.py | 1 + tests/python/dragon/test_py_cpp_interop.py | 1 + tests/python/dragon/test_wait_for_item.py | 1 + 6 files changed, 7 insertions(+), 1 deletion(-) diff --git a/example/py-cpp-exchange/dragon/driver.py b/example/py-cpp-exchange/dragon/driver.py index 9586221..229d663 100644 --- a/example/py-cpp-exchange/dragon/driver.py +++ b/example/py-cpp-exchange/dragon/driver.py @@ -8,6 +8,7 @@ import numpy as np from dragon.data.ddict import DDict from dragon.native.process import Process, ProcessTemplate + from radex.clients.core import DragonClient as Client from radex.handles.handles import IncomingHandle, OutgoingHandle diff --git a/tests/python/dragon/conftest.py b/tests/python/dragon/conftest.py index 54c330d..adf2921 100644 --- a/tests/python/dragon/conftest.py +++ b/tests/python/dragon/conftest.py @@ -5,6 +5,7 @@ from dragon.data.ddict import DDict from dragon.globalservices.api_setup import get_gs_ret_cuid from dragon.infrastructure.facts import DRAGON_BASE_DIR + from radex.clients.core import DragonClient as Client diff --git a/tests/python/dragon/test_client_init.py b/tests/python/dragon/test_client_init.py index e9a20dd..9f08601 100644 --- a/tests/python/dragon/test_client_init.py +++ b/tests/python/dragon/test_client_init.py @@ -1,8 +1,9 @@ import os import pytest -import radex.clients.core from dragon.data.ddict import DDict + +import radex.clients.core from radex.handles.handles import IncomingHandle, OutgoingHandle diff --git a/tests/python/dragon/test_put_and_get_item.py b/tests/python/dragon/test_put_and_get_item.py index 5692318..1ccc030 100644 --- a/tests/python/dragon/test_put_and_get_item.py +++ b/tests/python/dragon/test_put_and_get_item.py @@ -2,6 +2,7 @@ import numpy as np import pytest + from radex.handles.handles import IncomingHandle, OutgoingHandle diff --git a/tests/python/dragon/test_py_cpp_interop.py b/tests/python/dragon/test_py_cpp_interop.py index 75f3880..c098f54 100644 --- a/tests/python/dragon/test_py_cpp_interop.py +++ b/tests/python/dragon/test_py_cpp_interop.py @@ -7,6 +7,7 @@ import numpy as np import pytest from dragon.native.process import Popen + from radex.handles.handles import IncomingHandle, OutgoingHandle diff --git a/tests/python/dragon/test_wait_for_item.py b/tests/python/dragon/test_wait_for_item.py index 6872687..1594321 100644 --- a/tests/python/dragon/test_wait_for_item.py +++ b/tests/python/dragon/test_wait_for_item.py @@ -4,6 +4,7 @@ import time import pytest + from radex.clients.core import DragonClient from radex.handles.handles import IncomingHandle, OutgoingHandle From a67b14641791e96fc280cd754794232627a1f521 Mon Sep 17 00:00:00 2001 From: Matt Drozt Date: Tue, 18 Aug 2026 14:36:51 -0500 Subject: [PATCH 05/16] Remove filecmp for more fine grain error from test suite --- tests/examples/test_examples.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/tests/examples/test_examples.py b/tests/examples/test_examples.py index c9500f6..44556f8 100644 --- a/tests/examples/test_examples.py +++ b/tests/examples/test_examples.py @@ -1,7 +1,10 @@ -import filecmp - - def test_run_example(example, tmp_path): returncode, out, err = example.run(where=tmp_path) assert returncode == 0 - assert filecmp.cmp(out, example.expected_stdout) + + with ( + open(out, "r", encoding="utf-8") as fh, + open(example.expected_stdout, "r", encoding="utf-8") as xfh, + ): + for line, xline in zip(fh, xfh): + assert line == xline From 61207f58dc7ec435b98331260570e8e42fea8122 Mon Sep 17 00:00:00 2001 From: Matt Drozt Date: Tue, 18 Aug 2026 14:57:19 -0500 Subject: [PATCH 06/16] Debug CI --- tests/examples/test_examples.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/examples/test_examples.py b/tests/examples/test_examples.py index 44556f8..a6aa489 100644 --- a/tests/examples/test_examples.py +++ b/tests/examples/test_examples.py @@ -2,9 +2,19 @@ def test_run_example(example, tmp_path): returncode, out, err = example.run(where=tmp_path) assert returncode == 0 + print("Expected:") + print("---------") + with open(example.expected_stdout, "r", encoding="utf-8") as f: + print(f.read()) + + print("\nGot:") + print("----") + with open(out, "r", encoding="utf-8") as f: + print(f.read()) + with ( open(out, "r", encoding="utf-8") as fh, open(example.expected_stdout, "r", encoding="utf-8") as xfh, ): for line, xline in zip(fh, xfh): - assert line == xline + assert line.strip() == xline.strip() From f08833f8aa9dea39f39e383866781d03eae00555 Mon Sep 17 00:00:00 2001 From: Matt Drozt Date: Tue, 18 Aug 2026 15:11:38 -0500 Subject: [PATCH 07/16] Debug CI: checking stderr --- tests/examples/test_examples.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/examples/test_examples.py b/tests/examples/test_examples.py index a6aa489..0d5ed83 100644 --- a/tests/examples/test_examples.py +++ b/tests/examples/test_examples.py @@ -12,6 +12,11 @@ def test_run_example(example, tmp_path): with open(out, "r", encoding="utf-8") as f: print(f.read()) + print("\nError:") + print("------") + with open(err, "r", encoding="utf-8") as f: + print(f.read()) + with ( open(out, "r", encoding="utf-8") as fh, open(example.expected_stdout, "r", encoding="utf-8") as xfh, From c9e87fb38fb2906892056a9fd1f065d63bf7daf7 Mon Sep 17 00:00:00 2001 From: Matt Drozt Date: Tue, 18 Aug 2026 17:54:27 -0500 Subject: [PATCH 08/16] ldd example --- .github/workflows/tests.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2ba0d7f..fc895da 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -81,6 +81,10 @@ jobs: cmake --build build -j cmake --install build + - name: Wat?? + run: | + ldd install/bin/examples/dragon-cpp-producer + - name: Run tests run: | pip install -r dev-resources/requirements-dev.txt From eaede6ca96a6a248e940f00aa495018e70b81a51 Mon Sep 17 00:00:00 2001 From: Matt Drozt Date: Wed, 19 Aug 2026 13:01:28 -0500 Subject: [PATCH 09/16] manually add dragon libs to lib path --- tests/examples/conftest.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/examples/conftest.py b/tests/examples/conftest.py index cadb56e..9b6e72a 100644 --- a/tests/examples/conftest.py +++ b/tests/examples/conftest.py @@ -103,8 +103,12 @@ def __init__( from dragon.globalservices.api_setup import get_gs_ret_cuid from dragon.native.machine import System as DrgSystem except ImportError: + libs_path = None marks.append(pytest.mark.skip(reason="This example requires dragon")) else: + dragon_path, *_ = dragon.__path__ + libs_path = pathlib.Path(dragon_path).absolute() / "lib" + NOT_ENOUGH_NODES = pytest.mark.skip( reason=f"Example requires an allocation of {num_nodes} node(s)" ) @@ -144,8 +148,18 @@ def __init__( super().__init__(directory, marks=marks) self._dragon_args = dragon_args + self._libs_path = libs_path def _run(self, cwd, out, err) -> int: + # FIXME: Ideally we can remove this env futzing when we figure out why + # the examples are linking twice against dragon. + env = dict(os.environ) + lib_key = "DYLD_LIBRARY_PATH" if sys.platform == "darwin" else "LD_LIBRARY_PATH" + prev_libs = env.get(lib_key, "") + if self._libs_path is not None: + dragon_libs = os.fspath(self._libs_path) + env[lib_key] = f"{dragon_libs}:{prev_libs}" if prev_libs else dragon_libs + return sp.run( [ sys.executable, @@ -156,6 +170,7 @@ def _run(self, cwd, out, err) -> int: os.fspath(self.driver), ], cwd=cwd, + env=env, stdout=out, stderr=err, ).returncode From 968797d6283d461e6c4a6574d6bd196aa1bab757 Mon Sep 17 00:00:00 2001 From: Matt Drozt Date: Wed, 19 Aug 2026 13:36:14 -0500 Subject: [PATCH 10/16] remove debug step from workflow --- .github/workflows/tests.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index fc895da..2ba0d7f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -81,10 +81,6 @@ jobs: cmake --build build -j cmake --install build - - name: Wat?? - run: | - ldd install/bin/examples/dragon-cpp-producer - - name: Run tests run: | pip install -r dev-resources/requirements-dev.txt From 8ce52101f8c709c4a60063e12d971d812b2ce207 Mon Sep 17 00:00:00 2001 From: Matt Drozt Date: Wed, 19 Aug 2026 13:36:34 -0500 Subject: [PATCH 11/16] remove debug step from test --- tests/examples/test_examples.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/tests/examples/test_examples.py b/tests/examples/test_examples.py index 0d5ed83..134e5f8 100644 --- a/tests/examples/test_examples.py +++ b/tests/examples/test_examples.py @@ -1,22 +1,6 @@ def test_run_example(example, tmp_path): returncode, out, err = example.run(where=tmp_path) assert returncode == 0 - - print("Expected:") - print("---------") - with open(example.expected_stdout, "r", encoding="utf-8") as f: - print(f.read()) - - print("\nGot:") - print("----") - with open(out, "r", encoding="utf-8") as f: - print(f.read()) - - print("\nError:") - print("------") - with open(err, "r", encoding="utf-8") as f: - print(f.read()) - with ( open(out, "r", encoding="utf-8") as fh, open(example.expected_stdout, "r", encoding="utf-8") as xfh, From 9df561966841cc557762d9e001a46b574ff7dc20 Mon Sep 17 00:00:00 2001 From: Matt Drozt Date: Thu, 20 Aug 2026 17:29:04 -0500 Subject: [PATCH 12/16] Working locally --- dev-resources/Makefile | 2 +- tests/examples/conftest.py | 14 -------------- tests/examples/test_examples.py | 34 ++++++++++++++++++++++++++++++++- 3 files changed, 34 insertions(+), 16 deletions(-) diff --git a/dev-resources/Makefile b/dev-resources/Makefile index 6a09377..982b1da 100644 --- a/dev-resources/Makefile +++ b/dev-resources/Makefile @@ -35,4 +35,4 @@ docs: docs-serve: @command -v doxygen >/dev/null 2>&1 || \ echo "warning: 'doxygen' not found on PATH; the C++ API reference will fail to build." - cd $(ROOT)/.. && $(PYTHON) -m mkdocs serve -a 0.0.0.0:8001 \ No newline at end of file + cd $(ROOT)/.. && $(PYTHON) -m mkdocs serve -a 0.0.0.0:8001 diff --git a/tests/examples/conftest.py b/tests/examples/conftest.py index 9b6e72a..263b9b3 100644 --- a/tests/examples/conftest.py +++ b/tests/examples/conftest.py @@ -106,9 +106,6 @@ def __init__( libs_path = None marks.append(pytest.mark.skip(reason="This example requires dragon")) else: - dragon_path, *_ = dragon.__path__ - libs_path = pathlib.Path(dragon_path).absolute() / "lib" - NOT_ENOUGH_NODES = pytest.mark.skip( reason=f"Example requires an allocation of {num_nodes} node(s)" ) @@ -148,18 +145,8 @@ def __init__( super().__init__(directory, marks=marks) self._dragon_args = dragon_args - self._libs_path = libs_path def _run(self, cwd, out, err) -> int: - # FIXME: Ideally we can remove this env futzing when we figure out why - # the examples are linking twice against dragon. - env = dict(os.environ) - lib_key = "DYLD_LIBRARY_PATH" if sys.platform == "darwin" else "LD_LIBRARY_PATH" - prev_libs = env.get(lib_key, "") - if self._libs_path is not None: - dragon_libs = os.fspath(self._libs_path) - env[lib_key] = f"{dragon_libs}:{prev_libs}" if prev_libs else dragon_libs - return sp.run( [ sys.executable, @@ -170,7 +157,6 @@ def _run(self, cwd, out, err) -> int: os.fspath(self.driver), ], cwd=cwd, - env=env, stdout=out, stderr=err, ).returncode diff --git a/tests/examples/test_examples.py b/tests/examples/test_examples.py index 134e5f8..8f179fe 100644 --- a/tests/examples/test_examples.py +++ b/tests/examples/test_examples.py @@ -1,4 +1,36 @@ -def test_run_example(example, tmp_path): +import os +import pathlib +import sys + + +def test_run_example(example, tmp_path, monkeypatch): + # >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> + # FIXME: Ideally we can remove this env futzing when we figure out why + # the examples are linking twice against dragon. + # ================================================================================= + ld_lib_path = "DYLD_LIBRARY_PATH" if sys.platform == "darwin" else "LD_LIBRARY_PATH" + try: + import dragon + except ImportError: + pytest.xfail( + reason=( + f"Dragon library `libdragon.so` must be on {ld_lib_path} " + "to run the examples due to unresolved linking errors" + ) + ) + else: + dragon_path, *_ = dragon.__path__ + drg_libs_paths = ":".join( + os.fspath(p) for p in pathlib.Path(dragon_path).absolute().glob("lib*") + ) + + prev_ld_lib_path = os.environ.get(ld_lib_path, "") + new_ld_lib_path = ( + f"{drg_libs_paths}:{prev_ld_lib_path}" if prev_ld_lib_path else dragon_libs + ) + monkeypatch.setenv(ld_lib_path, new_ld_lib_path) + # <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< + returncode, out, err = example.run(where=tmp_path) assert returncode == 0 with ( From 8981ac829d810df4511a558a9d9529ce71627b3b Mon Sep 17 00:00:00 2001 From: Matt Drozt Date: Thu, 20 Aug 2026 17:50:56 -0500 Subject: [PATCH 13/16] Add debug output back --- tests/examples/test_examples.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/examples/test_examples.py b/tests/examples/test_examples.py index 8f179fe..6bd018c 100644 --- a/tests/examples/test_examples.py +++ b/tests/examples/test_examples.py @@ -32,6 +32,13 @@ def test_run_example(example, tmp_path, monkeypatch): # <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< returncode, out, err = example.run(where=tmp_path) + print("Out:") + with open(out, "r", encoding="utf-8") as f: + print(f.read()) + print("Err:") + with open(err, "r", encoding="utf-8") as f: + print(f.read()) + assert returncode == 0 with ( open(out, "r", encoding="utf-8") as fh, From f10ac09fd3652164352f7e61527e81265ceb3f94 Mon Sep 17 00:00:00 2001 From: Matt Drozt Date: Thu, 20 Aug 2026 18:03:13 -0500 Subject: [PATCH 14/16] Install dragon as non-editable --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3d29ac8..2c55674 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -63,7 +63,7 @@ jobs: - name: Install DragonHPC run: | git clone https://github.com/DragonHPC/dragon.git - pushd dragon && git checkout $_DRAGON_VERSION && pushd devtools && source VARIABLES && popd && pip install -e src/ + pushd dragon && git checkout $_DRAGON_VERSION && pushd devtools && source VARIABLES && popd && pip install src/ - name: Install Redis backend run: | From 9522b2c9c581ce6d2b49628faac6e5a47e341eb4 Mon Sep 17 00:00:00 2001 From: Matt Drozt Date: Thu, 20 Aug 2026 18:24:19 -0500 Subject: [PATCH 15/16] remove debug output one last time --- tests/examples/test_examples.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/tests/examples/test_examples.py b/tests/examples/test_examples.py index 6bd018c..8f179fe 100644 --- a/tests/examples/test_examples.py +++ b/tests/examples/test_examples.py @@ -32,13 +32,6 @@ def test_run_example(example, tmp_path, monkeypatch): # <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< returncode, out, err = example.run(where=tmp_path) - print("Out:") - with open(out, "r", encoding="utf-8") as f: - print(f.read()) - print("Err:") - with open(err, "r", encoding="utf-8") as f: - print(f.read()) - assert returncode == 0 with ( open(out, "r", encoding="utf-8") as fh, From fc333e26852a987cbb730a3be3b969bad03d2507 Mon Sep 17 00:00:00 2001 From: Matt Drozt Date: Fri, 21 Aug 2026 17:45:52 -0500 Subject: [PATCH 16/16] Better FIXME message --- tests/examples/test_examples.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/examples/test_examples.py b/tests/examples/test_examples.py index 8f179fe..ab88313 100644 --- a/tests/examples/test_examples.py +++ b/tests/examples/test_examples.py @@ -5,8 +5,14 @@ def test_run_example(example, tmp_path, monkeypatch): # >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> - # FIXME: Ideally we can remove this env futzing when we figure out why - # the examples are linking twice against dragon. + # FIXME: Ideally we can remove this env futzing when we figure out how to + # properly set the rpath on the examples such that `libdragon.so` + # does not need to be present on + # `LD_LIBRARY_PATH`/`DYLD_LIBRARY_PATH`. For now we can just add it + # based on the install site of the dragon package. There is likely a + # similar error with `libsmartredis.so` as well, but our CI appends + # the path to the library look up env var already, so we do not need + # to do that here. # ================================================================================= ld_lib_path = "DYLD_LIBRARY_PATH" if sys.platform == "darwin" else "LD_LIBRARY_PATH" try: