From b78c82f80bf4f204f717a7ac46335479209c22a1 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Tue, 25 Nov 2025 23:02:48 +0100 Subject: [PATCH 1/6] add instructions how to activate for development --- deltachat-rpc-client/README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/deltachat-rpc-client/README.md b/deltachat-rpc-client/README.md index 9777e06189..6b3ef8caf2 100644 --- a/deltachat-rpc-client/README.md +++ b/deltachat-rpc-client/README.md @@ -30,6 +30,15 @@ $ pip install . Additional arguments to `tox` are passed to pytest, e.g. `tox -- -s` does not capture test output. + +## Activating current checkout of deltachat-rpc-client and -server for development + +Go to root repository directory and run: +``` + scripts/make-rpc-testenv.sh + source venv/bin/activate +``` + ## Using in REPL Setup a development environment: From 134b1fa6bcfd5dc06dc17886e26cc7cca147c328 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Tue, 25 Nov 2025 23:28:22 +0100 Subject: [PATCH 2/6] add cross-core tests --- .../src/deltachat_rpc_client/pytestplugin.py | 106 ++++++++++++++++++ deltachat-rpc-client/tests/test_cross_core.py | 28 +++++ 2 files changed, 134 insertions(+) create mode 100644 deltachat-rpc-client/tests/test_cross_core.py diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py b/deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py index add7d624b9..f9993264e7 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py @@ -3,9 +3,13 @@ from __future__ import annotations import os +import platform import random +import subprocess +import sys from typing import AsyncGenerator, Optional +import execnet import py import pytest @@ -197,3 +201,105 @@ def indent(self, msg: str) -> None: print(" " + msg) return Printer() + + +# +# support for testing against different deltachat-rpc-server/clients +# installed into a temporary virtualenv and connected via 'execnet' channels +# + + +@pytest.fixture(scope="session") +def get_core_python_env(tmp_path_factory): + if platform.system() == "Windows": + pytest.skip("cross-core-version testing not available on Windows") + + envs = {} + + def get_core_python(core_version): + venv = envs.get(core_version) + if venv: + return venv + venv = tmp_path_factory.mktemp(f"temp-{core_version}") + python = sys.executable + subprocess.check_call([python, "-m", "venv", venv]) + pip = venv.joinpath("bin", "pip") + pkgs = [f"deltachat-rpc-server=={core_version}", f"deltachat-rpc-client=={core_version}"] + subprocess.check_call([pip, "install", "pytest"] + pkgs) + + envs[core_version] = venv + return venv + + return get_core_python + + +@pytest.fixture +def alice_and_remote_bob(tmp_path, acfactory, get_core_python_env): + """return local Alice account, a contact to bob, and a remote 'eval' function for bob. + + The 'eval' function allows to remote-execute arbitrary expressions + that can use the `bob` online account, and the `bob_contact_alice`. + """ + + def factory(core_version): + venv = get_core_python_env(core_version) + python = venv.joinpath("bin", "python") + gw = execnet.makegateway(f"popen//python={python}") + + accounts_dir = str(tmp_path.joinpath("account1_venv1")) + channel = gw.remote_exec(remote_bob_loop) + cm = os.environ.get("CHATMAIL_DOMAIN") + + # trigger getting an online account on bob's side + channel.send((accounts_dir, str(venv.joinpath("bin", "deltachat-rpc-server")), cm)) + + # meanwhile get a local alice account + alice = acfactory.get_online_account() + channel.send(alice.self_contact.make_vcard()) + + # wait for bob to have started + sysinfo = channel.receive() + assert sysinfo == f"v{core_version}" + bob_vcard = channel.receive() + [alice_contact_bob] = alice.import_vcard(bob_vcard) + + def eval(eval_str): + channel.send(eval_str) + return channel.receive() + + return alice, alice_contact_bob, eval + + return factory + + +def remote_bob_loop(channel): + import os + import pathlib + + from deltachat_rpc_client import DeltaChat, Rpc + from deltachat_rpc_client.pytestplugin import ACFactory + + accounts_dir, rpc_server_path, chatmail_domain = channel.receive() + os.environ["CHATMAIL_DOMAIN"] = chatmail_domain + bin_path = str(pathlib.Path(rpc_server_path).parent) + os.environ["PATH"] = bin_path + ":" + os.environ["PATH"] + + rpc = Rpc(accounts_dir=accounts_dir) + with rpc: + dc = DeltaChat(rpc) + channel.send(dc.rpc.get_system_info()["deltachat_core_version"]) + acfactory = ACFactory(dc) + bob = acfactory.get_online_account() + alice_vcard = channel.receive() + [alice_contact] = bob.import_vcard(alice_vcard) + ns = {"bob": bob, "bob_contact_alice": alice_contact} + channel.send(bob.self_contact.make_vcard()) + + while 1: + eval_str = channel.receive() + res = eval(eval_str, ns) + try: + channel.send(res) + except Exception: + # some unserializable result + channel.send(None) diff --git a/deltachat-rpc-client/tests/test_cross_core.py b/deltachat-rpc-client/tests/test_cross_core.py new file mode 100644 index 0000000000..bb6eba9163 --- /dev/null +++ b/deltachat-rpc-client/tests/test_cross_core.py @@ -0,0 +1,28 @@ +import pytest + + +@pytest.mark.parametrize("version", ["2.20.0", "2.10.0"]) +def test_qr_setup_contact(alice_and_remote_bob, version) -> None: + alice, alice_contact_bob, remote_eval = alice_and_remote_bob(version) + + qr_code = alice.get_qr_code() + remote_eval(f"bob.secure_join({qr_code!r})") + alice.wait_for_securejoin_inviter_success() + + # Test that Alice verified Bob's profile. + alice_contact_bob_snapshot = alice_contact_bob.get_snapshot() + assert alice_contact_bob_snapshot.is_verified + + remote_eval("bob.wait_for_securejoin_joiner_success()") + + # Test that Bob verified Alice's profile. + assert remote_eval("bob_contact_alice.get_snapshot().is_verified") + + +def test_send_and_receive_message(alice_and_remote_bob) -> None: + alice, alice_contact_bob, remote_eval = alice_and_remote_bob("2.20.0") + + remote_eval("bob_contact_alice.create_chat().send_text('hello')") + + msg = alice.wait_for_incoming_msg() + assert msg.get_snapshot().text == "hello" From 66d70d84c75bfaeaa0e2fd134e7c34a1465b5c98 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Tue, 25 Nov 2025 23:39:16 +0100 Subject: [PATCH 3/6] show which deltachat-rpc-server is used when pytestplugin from deltachat-rpc-client is used --- .../src/deltachat_rpc_client/pytestplugin.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py b/deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py index f9993264e7..f4c5e6d4e5 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py @@ -3,6 +3,7 @@ from __future__ import annotations import os +import pathlib import platform import random import subprocess @@ -24,6 +25,18 @@ """ +def pytest_report_header(): + for base in os.get_exec_path(): + fn = pathlib.Path(base).joinpath(base, "deltachat-rpc-server") + if fn.exists(): + proc = subprocess.Popen([str(fn), "--version"], stderr=subprocess.PIPE) + proc.wait() + version = proc.stderr.read().decode().strip() + return f"deltachat-rpc-server: {fn} [{version}]" + + return None + + class ACFactory: """Test account factory.""" From 8a9d56bde82431e056c845d1c5a5dffd3b86b0ea Mon Sep 17 00:00:00 2001 From: holger krekel Date: Tue, 25 Nov 2025 23:46:44 +0100 Subject: [PATCH 4/6] merge test_rpc_virtual into new cross-core testing file --- deltachat-rpc-client/tests/test_cross_core.py | 15 ++++++++++++++ .../tests/test_rpc_virtual.py | 20 ------------------- 2 files changed, 15 insertions(+), 20 deletions(-) delete mode 100644 deltachat-rpc-client/tests/test_rpc_virtual.py diff --git a/deltachat-rpc-client/tests/test_cross_core.py b/deltachat-rpc-client/tests/test_cross_core.py index bb6eba9163..3414d85f2c 100644 --- a/deltachat-rpc-client/tests/test_cross_core.py +++ b/deltachat-rpc-client/tests/test_cross_core.py @@ -1,5 +1,20 @@ +import subprocess + import pytest +from deltachat_rpc_client import DeltaChat, Rpc + + +def test_install_venv_and_use_other_core(tmp_path, get_core_python_env): + venv = get_core_python_env("2.20.0") + python = venv / "bin" / "python" + subprocess.check_call([python, "-m", "pip", "install", "deltachat-rpc-server==2.20.0"]) + rpc = Rpc(accounts_dir=tmp_path.joinpath("accounts"), rpc_server_path=venv.joinpath("bin", "deltachat-rpc-server")) + + with rpc: + dc = DeltaChat(rpc) + assert dc.rpc.get_system_info()["deltachat_core_version"] == "v2.20.0" + @pytest.mark.parametrize("version", ["2.20.0", "2.10.0"]) def test_qr_setup_contact(alice_and_remote_bob, version) -> None: diff --git a/deltachat-rpc-client/tests/test_rpc_virtual.py b/deltachat-rpc-client/tests/test_rpc_virtual.py deleted file mode 100644 index e1d988b346..0000000000 --- a/deltachat-rpc-client/tests/test_rpc_virtual.py +++ /dev/null @@ -1,20 +0,0 @@ -import subprocess -import sys -from platform import system # noqa - -import pytest - -from deltachat_rpc_client import DeltaChat, Rpc - - -@pytest.mark.skipif("system() == 'Windows'") -def test_install_venv_and_use_other_core(tmp_path): - venv = tmp_path.joinpath("venv1") - subprocess.check_call([sys.executable, "-m", "venv", venv]) - python = venv / "bin" / "python" - subprocess.check_call([python, "-m", "pip", "install", "deltachat-rpc-server==2.20.0"]) - rpc = Rpc(accounts_dir=tmp_path.joinpath("accounts"), rpc_server_path=venv.joinpath("bin", "deltachat-rpc-server")) - - with rpc: - dc = DeltaChat(rpc) - assert dc.rpc.get_system_info()["deltachat_core_version"] == "v2.20.0" From de1cf2f894963aad9d56a2f0d01943e5fa22eaeb Mon Sep 17 00:00:00 2001 From: holger krekel Date: Wed, 26 Nov 2025 15:29:27 +0100 Subject: [PATCH 5/6] make cross-core tests work on windows --- .../src/deltachat_rpc_client/pytestplugin.py | 69 +++++++++++++------ .../src/deltachat_rpc_client/rpc.py | 2 +- deltachat-rpc-client/tests/test_cross_core.py | 13 ++-- 3 files changed, 57 insertions(+), 27 deletions(-) diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py b/deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py index f4c5e6d4e5..9a002d9fc0 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py @@ -222,28 +222,49 @@ def indent(self, msg: str) -> None: # +def find_path(venv, name): + is_windows = platform.system() == "Windows" + bin = venv / ("bin" if not is_windows else "Scripts") + + tryadd = [""] + if is_windows: + tryadd += os.environ["PATHEXT"].split(os.pathsep) + for ext in tryadd: + p = bin.joinpath(name + ext) + if p.exists(): + return str(p) + + return None + + @pytest.fixture(scope="session") def get_core_python_env(tmp_path_factory): - if platform.system() == "Windows": - pytest.skip("cross-core-version testing not available on Windows") + """Return a factory to create virtualenv environments with rpc server/client packages + installed. + + The factory takes a version and returns a (python_path, rpc_server_path) tuple + of the respective binaries in the virtualenv. + """ envs = {} - def get_core_python(core_version): + def get_versioned_venv(core_version): venv = envs.get(core_version) - if venv: - return venv - venv = tmp_path_factory.mktemp(f"temp-{core_version}") - python = sys.executable - subprocess.check_call([python, "-m", "venv", venv]) - pip = venv.joinpath("bin", "pip") - pkgs = [f"deltachat-rpc-server=={core_version}", f"deltachat-rpc-client=={core_version}"] - subprocess.check_call([pip, "install", "pytest"] + pkgs) + if not venv: + venv = tmp_path_factory.mktemp(f"temp-{core_version}") + subprocess.check_call([sys.executable, "-m", "venv", venv]) + + python = find_path(venv, "python") + pkgs = [f"deltachat-rpc-server=={core_version}", f"deltachat-rpc-client=={core_version}", "pytest"] + subprocess.check_call([python, "-m", "pip", "install"] + pkgs) - envs[core_version] = venv - return venv + envs[core_version] = venv + python = find_path(venv, "python") + rpc_server_path = find_path(venv, "deltachat-rpc-server") + print(f"python={python}\nrpc_server={rpc_server_path}") + return python, rpc_server_path - return get_core_python + return get_versioned_venv @pytest.fixture @@ -255,8 +276,7 @@ def alice_and_remote_bob(tmp_path, acfactory, get_core_python_env): """ def factory(core_version): - venv = get_core_python_env(core_version) - python = venv.joinpath("bin", "python") + python, rpc_server_path = get_core_python_env(core_version) gw = execnet.makegateway(f"popen//python={python}") accounts_dir = str(tmp_path.joinpath("account1_venv1")) @@ -264,7 +284,7 @@ def factory(core_version): cm = os.environ.get("CHATMAIL_DOMAIN") # trigger getting an online account on bob's side - channel.send((accounts_dir, str(venv.joinpath("bin", "deltachat-rpc-server")), cm)) + channel.send((accounts_dir, str(rpc_server_path), cm)) # meanwhile get a local alice account alice = acfactory.get_online_account() @@ -286,18 +306,27 @@ def eval(eval_str): def remote_bob_loop(channel): + # This function executes with versioned + # deltachat-rpc-client/server packages + # installed into the virtualenv. + # + # The "channel" argument is a send/receive pipe + # to the process that runs the corresponding remote_exec(remote_bob_loop) + import os - import pathlib from deltachat_rpc_client import DeltaChat, Rpc from deltachat_rpc_client.pytestplugin import ACFactory accounts_dir, rpc_server_path, chatmail_domain = channel.receive() os.environ["CHATMAIL_DOMAIN"] = chatmail_domain - bin_path = str(pathlib.Path(rpc_server_path).parent) - os.environ["PATH"] = bin_path + ":" + os.environ["PATH"] + # older core versions don't support specifying rpc_server_path + # so we can't just pass `rpc_server_path` argument to Rpc constructor + basepath = os.path.dirname(rpc_server_path) + os.environ["PATH"] = os.pathsep.join([basepath, os.environ["PATH"]]) rpc = Rpc(accounts_dir=accounts_dir) + with rpc: dc = DeltaChat(rpc) channel.send(dc.rpc.get_system_info()["deltachat_core_version"]) diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/rpc.py b/deltachat-rpc-client/src/deltachat_rpc_client/rpc.py index faf0edaac9..8ff5c1e895 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/rpc.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/rpc.py @@ -57,7 +57,7 @@ class Rpc: def __init__(self, accounts_dir: Optional[str] = None, rpc_server_path="deltachat-rpc-server", **kwargs): """Initialize RPC client. - The given arguments will be passed to subprocess.Popen(). + The 'kwargs' arguments will be passed to subprocess.Popen(). """ if accounts_dir: kwargs["env"] = { diff --git a/deltachat-rpc-client/tests/test_cross_core.py b/deltachat-rpc-client/tests/test_cross_core.py index 3414d85f2c..babad0e6e6 100644 --- a/deltachat-rpc-client/tests/test_cross_core.py +++ b/deltachat-rpc-client/tests/test_cross_core.py @@ -6,18 +6,18 @@ def test_install_venv_and_use_other_core(tmp_path, get_core_python_env): - venv = get_core_python_env("2.20.0") - python = venv / "bin" / "python" - subprocess.check_call([python, "-m", "pip", "install", "deltachat-rpc-server==2.20.0"]) - rpc = Rpc(accounts_dir=tmp_path.joinpath("accounts"), rpc_server_path=venv.joinpath("bin", "deltachat-rpc-server")) + python, rpc_server_path = get_core_python_env("2.24.0") + subprocess.check_call([python, "-m", "pip", "install", "deltachat-rpc-server==2.24.0"]) + rpc = Rpc(accounts_dir=tmp_path.joinpath("accounts"), rpc_server_path=rpc_server_path) with rpc: dc = DeltaChat(rpc) - assert dc.rpc.get_system_info()["deltachat_core_version"] == "v2.20.0" + assert dc.rpc.get_system_info()["deltachat_core_version"] == "v2.24.0" -@pytest.mark.parametrize("version", ["2.20.0", "2.10.0"]) +@pytest.mark.parametrize("version", ["2.24.0"]) def test_qr_setup_contact(alice_and_remote_bob, version) -> None: + """Test other-core Bob profile can do securejoin with Alice on current core.""" alice, alice_contact_bob, remote_eval = alice_and_remote_bob(version) qr_code = alice.get_qr_code() @@ -35,6 +35,7 @@ def test_qr_setup_contact(alice_and_remote_bob, version) -> None: def test_send_and_receive_message(alice_and_remote_bob) -> None: + """Test other-core Bob profile can send a message to Alice on current core.""" alice, alice_contact_bob, remote_eval = alice_and_remote_bob("2.20.0") remote_eval("bob_contact_alice.create_chat().send_text('hello')") From 2925fad71f1fd466f3059ef5798a61b77c7c5f4e Mon Sep 17 00:00:00 2001 From: holger krekel Date: Thu, 27 Nov 2025 21:21:30 +0100 Subject: [PATCH 6/6] Update deltachat-rpc-client/README.md Co-authored-by: iequidoo <117991069+iequidoo@users.noreply.github.com> --- deltachat-rpc-client/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deltachat-rpc-client/README.md b/deltachat-rpc-client/README.md index 6b3ef8caf2..5672dd807d 100644 --- a/deltachat-rpc-client/README.md +++ b/deltachat-rpc-client/README.md @@ -35,8 +35,8 @@ Additional arguments to `tox` are passed to pytest, e.g. `tox -- -s` does not ca Go to root repository directory and run: ``` - scripts/make-rpc-testenv.sh - source venv/bin/activate +$ scripts/make-rpc-testenv.sh +$ source venv/bin/activate ``` ## Using in REPL