diff --git a/deltachat-rpc-client/README.md b/deltachat-rpc-client/README.md index 9777e06189..5672dd807d 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: diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py b/deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py index add7d624b9..9a002d9fc0 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py @@ -3,9 +3,14 @@ from __future__ import annotations import os +import pathlib +import platform import random +import subprocess +import sys from typing import AsyncGenerator, Optional +import execnet import py import pytest @@ -20,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.""" @@ -197,3 +214,134 @@ 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 +# + + +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): + """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_versioned_venv(core_version): + venv = envs.get(core_version) + 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 + 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_versioned_venv + + +@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): + 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")) + 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(rpc_server_path), 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): + # 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 + + 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 + + # 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"]) + 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/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 new file mode 100644 index 0000000000..babad0e6e6 --- /dev/null +++ b/deltachat-rpc-client/tests/test_cross_core.py @@ -0,0 +1,44 @@ +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): + 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.24.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() + 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: + """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')") + + msg = alice.wait_for_incoming_msg() + assert msg.get_snapshot().text == "hello" 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"