From b7d0d75b7c02ca88c630f3c727869afcfbbce2e8 Mon Sep 17 00:00:00 2001 From: Vincent Michel Date: Wed, 23 Jun 2021 18:03:26 +0200 Subject: [PATCH 01/35] Attempt a migration to qtrio --- parsec/core/gui/app.py | 51 ++++------ parsec/core/gui/instance_widget.py | 11 ++- parsec/core/gui/trio_thread.py | 144 +++++------------------------ setup.py | 2 +- 4 files changed, 49 insertions(+), 159 deletions(-) diff --git a/parsec/core/gui/app.py b/parsec/core/gui/app.py index 01aa63e46d0..3d4491e4db6 100644 --- a/parsec/core/gui/app.py +++ b/parsec/core/gui/app.py @@ -2,11 +2,12 @@ import sys import signal -from queue import Queue from typing import Optional from contextlib import contextmanager from enum import Enum + import trio +import qtrio from structlog import get_logger from PyQt5.QtCore import QTimer, Qt from PyQt5.QtWidgets import QApplication @@ -28,7 +29,7 @@ from parsec.core.gui.new_version import CheckNewVersion from parsec.core.gui.systray import systray_available, Systray from parsec.core.gui.main_window import MainWindow - from parsec.core.gui.trio_thread import ThreadSafeQtSignal, run_trio_thread + from parsec.core.gui.trio_thread import ThreadSafeQtSignal, run_trio_job_scheduler except ImportError as exc: raise ModuleNotFoundError( """PyQt forms haven't been generated. @@ -50,7 +51,7 @@ def _before_quit(): IPCServerStartupOutcome = Enum("IPCServerStartupOutcome", "STARTED ALREADY_RUNNING ERROR") -async def _run_ipc_server(config, main_window, start_arg, result_queue): +async def _run_ipc_server(config, main_window, start_arg, task_status=trio.TASK_STATUS_IGNORED): try: new_instance_needed_qt = ThreadSafeQtSignal(main_window, "new_instance_needed", object) foreground_needed_qt = ThreadSafeQtSignal(main_window, "foreground_needed") @@ -69,7 +70,7 @@ async def _cmd_handler(cmd): config.ipc_socket_file, win32_mutex_name=config.ipc_win32_mutex_name, ): - result_queue.put_nowait(IPCServerStartupOutcome.STARTED) + task_status.started(IPCServerStartupOutcome.STARTED) await trio.sleep_forever() except IPCServerAlreadyRunning: @@ -87,12 +88,12 @@ async def _cmd_handler(cmd): continue # We have successfuly noticed the other running application - result_queue.put_nowait(IPCServerStartupOutcome.ALREADY_RUNNING) + task_status.started(IPCServerStartupOutcome.ALREADY_RUNNING) break - except Exception: - result_queue.put_nowait(IPCServerStartupOutcome.ERROR) - # Let the exception bubble up so QtToTrioJob logged it as an unexpected error + except Exception as exc: + task_status.started(IPCServerStartupOutcome.ERROR) + logger.exection(exc) raise @@ -140,16 +141,18 @@ def run_gui(config: CoreConfig, start_arg: Optional[str] = None, diagnose: bool QApplication.setHighDpiScaleFactorRoundingPolicy( Qt.HighDpiScaleFactorRoundingPolicy.PassThrough ) + return qtrio.run(_run_gui, config, start_arg, diagnose) - app = ParsecApp() +async def _run_gui(config: CoreConfig, start_arg: str = None, diagnose: bool = False): + app = ParsecApp() app.load_stylesheet() app.load_font() lang_key = lang.switch_language(config) event_bus = EventBus() - with run_trio_thread() as jobs_ctx: + async with run_trio_job_scheduler() as jobs_ctx: win = MainWindow( jobs_ctx=jobs_ctx, event_bus=event_bus, @@ -157,27 +160,9 @@ def run_gui(config: CoreConfig, start_arg: Optional[str] = None, diagnose: bool minimize_on_close=config.gui_tray_enabled and systray_available(), ) - result_queue = Queue(maxsize=1) - - class ThreadSafeNoQtSignal(ThreadSafeQtSignal): - def __init__(self): - self.qobj = None - self.signal_name = "" - self.args_types = () - - def emit(self, *args): - pass - - jobs_ctx.submit_job( - ThreadSafeNoQtSignal(), - ThreadSafeNoQtSignal(), - _run_ipc_server, - config, - win, - start_arg, - result_queue, - ) - if result_queue.get() == IPCServerStartupOutcome.ALREADY_RUNNING: + result = await jobs_ctx.nursery.start(_run_ipc_server, config, win, start_arg) + + if result == IPCServerStartupOutcome.ALREADY_RUNNING: # Another instance of Parsec already started, nothing more to do return @@ -234,7 +219,7 @@ def kill_window(*args): if diagnose: with fail_on_first_exception(kill_window): - return app.exec_() + await trio.sleep_forever() else: with log_pyqt_exceptions(): - return app.exec_() + await trio.sleep_forever() diff --git a/parsec/core/gui/instance_widget.py b/parsec/core/gui/instance_widget.py index b13f4c51211..19929d801f0 100644 --- a/parsec/core/gui/instance_widget.py +++ b/parsec/core/gui/instance_widget.py @@ -19,7 +19,11 @@ MountpointWinfspNotAvailable, ) -from parsec.core.gui.trio_thread import QtToTrioJobScheduler, ThreadSafeQtSignal +from parsec.core.gui.trio_thread import ( + QtToTrioJobScheduler, + ThreadSafeQtSignal, + run_trio_job_scheduler, +) from parsec.core.gui.parsec_application import ParsecApp from parsec.core.gui.custom_dialogs import show_error, show_info_link from parsec.core.gui.lang import translate as _ @@ -37,10 +41,9 @@ async def _do_run_core(config, device, qt_on_ready): async with logged_core_factory(config=config, device=device, event_bus=None) as core: # Create our own job scheduler allows us to cancel all pending # jobs depending on us when we logout - core_jobs_ctx = QtToTrioJobScheduler() - async with trio.open_service_nursery() as nursery: - await nursery.start(core_jobs_ctx._start) + async with run_trio_job_scheduler() as core_jobs_ctx: qt_on_ready.emit(core, core_jobs_ctx) + await trio.sleep_forever() def ensure_macfuse_available_or_show_dialogue(window): diff --git a/parsec/core/gui/trio_thread.py b/parsec/core/gui/trio_thread.py index d9bcff2c3f1..357520dde39 100644 --- a/parsec/core/gui/trio_thread.py +++ b/parsec/core/gui/trio_thread.py @@ -1,14 +1,14 @@ # Parsec Cloud (https://parsec.cloud) Copyright (c) AGPLv3 2016-2021 Scille SAS import threading -from contextlib import contextmanager +from contextlib import asynccontextmanager from inspect import iscoroutinefunction, signature import trio from structlog import get_logger from parsec.core.fs import FSError from parsec.core.mountpoint import MountpointError -from parsec.utils import trio_run, split_multi_error +from parsec.utils import split_multi_error from PyQt5.QtCore import pyqtBoundSignal, Q_ARG, QMetaObject, Qt @@ -44,13 +44,15 @@ def __repr__(self): class QtToTrioJob: - def __init__(self, trio_token, fn, args, kwargs, qt_on_success, qt_on_error): - self._trio_token = trio_token + def __init__(self, fn, args, kwargs, qt_on_success, qt_on_error): + # Fool-proof sanity check, signals must be wrapped in `ThreadSafeQtSignal` + assert not [x for x in args if isinstance(x, pyqtBoundSignal)] + assert not [v for v in kwargs.values() if isinstance(v, pyqtBoundSignal)] assert isinstance(qt_on_success, ThreadSafeQtSignal) assert qt_on_success.args_types in ((), (QtToTrioJob,)) - self._qt_on_success = qt_on_success assert isinstance(qt_on_error, ThreadSafeQtSignal) assert qt_on_error.args_types in ((), (QtToTrioJob,)) + self._qt_on_success = qt_on_success self._qt_on_error = qt_on_error self._fn = fn self._args = args @@ -145,66 +147,16 @@ def _set_done(self): signal = self._qt_on_success if self.is_ok() else self._qt_on_error signal.emit(self) if signal.args_types else signal.emit() - def cancel_and_join(self): - assert self.cancel_scope - try: - trio.from_thread.run_sync(self.cancel_scope.cancel, trio_token=self._trio_token) - except trio.RunFinishedError: - pass - self._done.wait() + def cancel(self): + self.cancel_scope.cancel() class QtToTrioJobScheduler: - def __init__(self): - self._trio_token = None - self._cancel_scope = None - self.started = threading.Event() - self._stopped = trio.Event() + def __init__(self, nursery): + self.nursery = nursery self._throttling_scheduled_jobs = {} self._throttling_last_executed = {} - async def _start(self, *, task_status=trio.TASK_STATUS_IGNORED): - assert not self.started.is_set() - self._trio_token = trio.lowlevel.current_trio_token() - self._send_job_channel, recv_job_channel = trio.open_memory_channel(1) - try: - async with trio.open_service_nursery() as nursery, recv_job_channel: - self._cancel_scope = nursery.cancel_scope - self.started.set() - task_status.started() - while True: - job = await recv_job_channel.receive() - await nursery.start(job) - - finally: - self._stopped.set() - - async def _stop(self): - self._cancel_scope.cancel() - await self._send_job_channel.aclose() - await self._stopped.wait() - - def stop(self): - try: - trio.from_thread.run(self._stop, trio_token=self._trio_token) - except trio.RunFinishedError: - pass - - def _run_job(self, job, *args, sync=False): - try: - if sync: - return trio.from_thread.run_sync(job, *args, trio_token=self._trio_token) - else: - return trio.from_thread.run(job, *args, trio_token=self._trio_token) - - except trio.BrokenResourceError: - logger.info(f"The submitted job `{job}` won't run as the scheduler is stopped") - raise JobSchedulerNotAvailable("The job scheduler is stopped") - - except trio.RunFinishedError: - logger.info(f"The submitted job `{job}` won't run as the trio loop is not running") - raise JobSchedulerNotAvailable("The trio loop is not running") - def submit_throttled_job( self, throttling_id: str, delay: float, qt_on_success, qt_on_error, fn, *args, **kwargs ): @@ -215,13 +167,10 @@ def submit_throttled_job( Submitting a job with an already sheduled `throttling_id` will lead to a single execution of the last provided job parameters at the soonest delay. """ - # Fool-proof sanity check, signals must be wrapped in `ThreadSafeQtSignal` - assert not [x for x in args if isinstance(x, pyqtBoundSignal)] - assert not [v for v in kwargs.values() if isinstance(v, pyqtBoundSignal)] async def _throttled_execute(task_status=trio.TASK_STATUS_IGNORED): # Create the job but don't execute it: we have to handle throttle first ! - job = QtToTrioJob(self._trio_token, fn, args, kwargs, qt_on_success, qt_on_error) + job = QtToTrioJob(fn, args, kwargs, qt_on_success, qt_on_error) # Only modify `_throttling_scheduled_jobs` from the trio # thread to avoid concurrent acces with the Qt thread @@ -247,71 +196,24 @@ async def _throttled_execute(task_status=trio.TASK_STATUS_IGNORED): # Actually start the job execution self._throttling_last_executed[throttling_id] = trio.current_time() try: - await self._send_job_channel.send(job) + await self.nursery.start(job) except trio.BrokenResourceError: # Job scheduler has been closed, nothing more can be done pass - async def _send_throttle_job(): - # While inside this async function we are blocking the Qt thread - # hence we just submit `_throttled_execute` - await self._send_job_channel.send(_throttled_execute) - - try: - self._run_job(_send_throttle_job) - except JobSchedulerNotAvailable: - pass + self.nursery.start_soon(_throttled_execute) def submit_job(self, qt_on_success, qt_on_error, fn, *args, **kwargs): - # Fool-proof sanity check, signals must be wrapped in `ThreadSafeQtSignal` - assert not [x for x in args if isinstance(x, pyqtBoundSignal)] - assert not [v for v in kwargs.values() if isinstance(v, pyqtBoundSignal)] - job = QtToTrioJob(self._trio_token, fn, args, kwargs, qt_on_success, qt_on_error) - - async def _submit_job(): - # While inside this async function we are blocking the Qt thread - # hence we just wait for the job to start (to avoid concurrent - # crash if the job is cancelled) - await self._send_job_channel.send(job) - await job._started.wait() - - try: - self._run_job(_submit_job) - except JobSchedulerNotAvailable as exc: - job.set_cancelled(exc) - - return job - - # This method is only here for legacy purposes. - # It shouldn't NOT be used as running an async job synchronously - # might block the Qt loop for too long and cause the application - # to freeze. TODO: remove it later - - def run(self, afn, *args): - return self._run_job(afn, *args) - - # In contrast to the `run` method, it is acceptable to block - # the Qt loop while waiting for a synchronous job to finish - # as it shouldn't take too long (it might simply wait for - # a few scheduled trio task steps to finish). However, - # it shouln't be used too aggressively as it might still slow - # down the application. + assert not self.nursery._closed + job = QtToTrioJob(fn, args, kwargs, qt_on_success, qt_on_error) + self.nursery.start_soon(job) def run_sync(self, fn, *args): - return self._run_job(fn, *args, sync=True) - - -@contextmanager -def run_trio_thread(): - job_scheduler = QtToTrioJobScheduler() - thread = threading.Thread(target=trio_run, args=[job_scheduler._start]) - thread.setName("TrioLoop") - thread.start() - job_scheduler.started.wait() + assert not self.nursery._closed + return fn(*args) - try: - yield job_scheduler - finally: - job_scheduler.stop() - thread.join() +@asynccontextmanager +async def run_trio_job_scheduler(): + async with trio.open_service_nursery() as nursery: + yield QtToTrioJobScheduler(nursery) diff --git a/setup.py b/setup.py index 3286198b232..91c21fb35f9 100644 --- a/setup.py +++ b/setup.py @@ -318,7 +318,7 @@ def run(self): ] -PYQT_DEPS = ["PyQt5==5.15.2", "pyqt5-sip==12.8.1"] +PYQT_DEPS = ["PyQt5==5.15.2", "pyqt5-sip==12.8.1", "qtrio==0.4.2"] GUI_DEPS = [*PYQT_DEPS, "qrcode==6.1"] BABEL_DEP = "Babel==2.6.0" WHEEL_DEP = "wheel==0.34.2" From 059a9b5b868e69e4f553451f2f6e493c8791c139 Mon Sep 17 00:00:00 2001 From: Vincent Michel Date: Wed, 23 Jun 2021 18:39:23 +0200 Subject: [PATCH 02/35] Remove ThreadSafeQtSignal --- parsec/core/gui/app.py | 6 +-- parsec/core/gui/central_widget.py | 6 +-- parsec/core/gui/claim_device_widget.py | 34 ++++-------- parsec/core/gui/claim_user_widget.py | 34 ++++-------- parsec/core/gui/create_org_widget.py | 6 +-- parsec/core/gui/devices_widget.py | 12 ++--- parsec/core/gui/file_history_widget.py | 5 +- parsec/core/gui/files_widget.py | 52 +++++++++---------- parsec/core/gui/greet_device_widget.py | 31 ++++------- parsec/core/gui/greet_user_widget.py | 34 ++++-------- parsec/core/gui/instance_widget.py | 12 ++--- parsec/core/gui/new_version.py | 5 +- .../core/gui/timestamped_workspace_widget.py | 5 +- parsec/core/gui/trio_thread.py | 25 --------- parsec/core/gui/users_widget.py | 22 ++++---- parsec/core/gui/workspace_sharing_widget.py | 10 ++-- parsec/core/gui/workspaces_widget.py | 43 +++++++-------- 17 files changed, 124 insertions(+), 218 deletions(-) diff --git a/parsec/core/gui/app.py b/parsec/core/gui/app.py index 3d4491e4db6..1e9dc5d0121 100644 --- a/parsec/core/gui/app.py +++ b/parsec/core/gui/app.py @@ -29,7 +29,7 @@ from parsec.core.gui.new_version import CheckNewVersion from parsec.core.gui.systray import systray_available, Systray from parsec.core.gui.main_window import MainWindow - from parsec.core.gui.trio_thread import ThreadSafeQtSignal, run_trio_job_scheduler + from parsec.core.gui.trio_thread import run_trio_job_scheduler except ImportError as exc: raise ModuleNotFoundError( """PyQt forms haven't been generated. @@ -53,8 +53,8 @@ def _before_quit(): async def _run_ipc_server(config, main_window, start_arg, task_status=trio.TASK_STATUS_IGNORED): try: - new_instance_needed_qt = ThreadSafeQtSignal(main_window, "new_instance_needed", object) - foreground_needed_qt = ThreadSafeQtSignal(main_window, "foreground_needed") + new_instance_needed_qt = main_window.new_instance_needed + foreground_needed_qt = main_window.foreground_needed async def _cmd_handler(cmd): if cmd["cmd"] == IPCCommand.FOREGROUND: diff --git a/parsec/core/gui/central_widget.py b/parsec/core/gui/central_widget.py index b7d687f53b1..6f66adc0eb0 100644 --- a/parsec/core/gui/central_widget.py +++ b/parsec/core/gui/central_widget.py @@ -37,7 +37,7 @@ from parsec.core.gui.custom_widgets import Pixmap from parsec.core.gui.custom_dialogs import show_error from parsec.core.gui.ui.central_widget import Ui_CentralWidget -from parsec.core.gui.trio_thread import JobResultError, ThreadSafeQtSignal, QtToTrioJob +from parsec.core.gui.trio_thread import JobResultError, QtToTrioJob async def _do_get_organization_stats(core: LoggedCore) -> OrganizationStats: @@ -264,8 +264,8 @@ def _load_organization_stats(self, delay: float = 0) -> None: self.jobs_ctx.submit_throttled_job( "central_widget.load_organization_stats", delay, - ThreadSafeQtSignal(self, "organization_stats_success", QtToTrioJob), - ThreadSafeQtSignal(self, "organization_stats_error", QtToTrioJob), + self.organization_stats_success, + self.organization_stats_error, _do_get_organization_stats, core=self.core, ) diff --git a/parsec/core/gui/claim_device_widget.py b/parsec/core/gui/claim_device_widget.py index 47b3631627b..b7e428074e2 100644 --- a/parsec/core/gui/claim_device_widget.py +++ b/parsec/core/gui/claim_device_widget.py @@ -17,7 +17,7 @@ BackendNotAvailable, ) from parsec.core.gui import validators -from parsec.core.gui.trio_thread import JobResultError, ThreadSafeQtSignal, QtToTrioJob +from parsec.core.gui.trio_thread import JobResultError, QtToTrioJob from parsec.core.gui.desktop import get_default_device from parsec.core.gui.custom_dialogs import show_error, GreyedDialog, show_info from parsec.core.gui.lang import translate as _ @@ -214,17 +214,13 @@ def __init__(self, jobs_ctx, claimer): self.wait_peer_trust_error.connect(self._on_wait_peer_trust_error) self.get_greeter_sas_job = self.jobs_ctx.submit_job( - ThreadSafeQtSignal(self, "get_greeter_sas_success", QtToTrioJob), - ThreadSafeQtSignal(self, "get_greeter_sas_error", QtToTrioJob), - self.claimer.get_greeter_sas, + self.get_greeter_sas_success, self.get_greeter_sas_error, self.claimer.get_greeter_sas ) def _on_good_greeter_code_clicked(self): self.widget_greeter_code.setDisabled(True) self.signify_trust_job = self.jobs_ctx.submit_job( - ThreadSafeQtSignal(self, "signify_trust_success", QtToTrioJob), - ThreadSafeQtSignal(self, "signify_trust_error", QtToTrioJob), - self.claimer.signify_trust, + self.signify_trust_success, self.signify_trust_error, self.claimer.signify_trust ) def _on_wrong_greeter_code_clicked(self): @@ -276,9 +272,7 @@ def _on_get_claimer_sas_success(self, job): self.widget_claimer_code.setVisible(True) self.line_edit_claimer_code.setText(str(claimer_sas)) self.wait_peer_trust_job = self.jobs_ctx.submit_job( - ThreadSafeQtSignal(self, "wait_peer_trust_success", QtToTrioJob), - ThreadSafeQtSignal(self, "wait_peer_trust_error", QtToTrioJob), - self.claimer.wait_peer_trust, + self.wait_peer_trust_success, self.wait_peer_trust_error, self.claimer.wait_peer_trust ) def _on_get_claimer_sas_error(self, job): @@ -298,9 +292,7 @@ def _on_signify_trust_success(self, job): assert job.is_finished() assert job.status == "ok" self.get_claimer_sas_job = self.jobs_ctx.submit_job( - ThreadSafeQtSignal(self, "get_claimer_sas_success", QtToTrioJob), - ThreadSafeQtSignal(self, "get_claimer_sas_error", QtToTrioJob), - self.claimer.get_claimer_sas, + self.get_claimer_sas_success, self.get_claimer_sas_error, self.claimer.get_claimer_sas ) def _on_signify_trust_error(self, job): @@ -396,8 +388,8 @@ def _on_claim_clicked(self): self.widget_info.setDisabled(True) self.label_wait.show() self.claim_job = self.jobs_ctx.submit_job( - ThreadSafeQtSignal(self, "claim_success", QtToTrioJob), - ThreadSafeQtSignal(self, "claim_error", QtToTrioJob), + self.claim_success, + self.claim_error, self.claimer.claim_device, device_label=device_label, ) @@ -460,9 +452,7 @@ def _on_button_start_clicked(self): self.button_start.setDisabled(True) self.button_start.setText(_("TEXT_CLAIM_DEVICE_WAITING")) self.wait_peer_job = self.jobs_ctx.submit_job( - ThreadSafeQtSignal(self, "wait_peer_success", QtToTrioJob), - ThreadSafeQtSignal(self, "wait_peer_error", QtToTrioJob), - self.claimer.wait_peer, + self.wait_peer_success, self.wait_peer_error, self.claimer.wait_peer ) def _on_wait_peer_success(self, job): @@ -525,16 +515,14 @@ def __init__(self, jobs_ctx, config, addr): def _run_claimer(self): self.claimer_job = self.jobs_ctx.submit_job( - ThreadSafeQtSignal(self, "claimer_success", QtToTrioJob), - ThreadSafeQtSignal(self, "claimer_error", QtToTrioJob), + self.claimer_success, + self.claimer_error, self.claimer.run, addr=self.addr, config=self.config, ) self.retrieve_info_job = self.jobs_ctx.submit_job( - ThreadSafeQtSignal(self, "retrieve_info_success", QtToTrioJob), - ThreadSafeQtSignal(self, "retrieve_info_error", QtToTrioJob), - self.claimer.retrieve_info, + self.retrieve_info_success, self.retrieve_info_error, self.claimer.retrieve_info ) def _on_retrieve_info_success(self, job): diff --git a/parsec/core/gui/claim_user_widget.py b/parsec/core/gui/claim_user_widget.py index 3e2adbc05ce..c5d38f1e5b4 100644 --- a/parsec/core/gui/claim_user_widget.py +++ b/parsec/core/gui/claim_user_widget.py @@ -18,7 +18,7 @@ BackendNotAvailable, ) from parsec.core.gui import validators -from parsec.core.gui.trio_thread import JobResultError, ThreadSafeQtSignal, QtToTrioJob +from parsec.core.gui.trio_thread import JobResultError, QtToTrioJob from parsec.core.gui.desktop import get_default_device from parsec.core.gui.custom_dialogs import show_error, GreyedDialog, show_info from parsec.core.gui.lang import translate as _ @@ -253,17 +253,13 @@ def __init__(self, jobs_ctx, claimer): self.wait_peer_trust_error.connect(self._on_wait_peer_trust_error) self.get_greeter_sas_job = self.jobs_ctx.submit_job( - ThreadSafeQtSignal(self, "get_greeter_sas_success", QtToTrioJob), - ThreadSafeQtSignal(self, "get_greeter_sas_error", QtToTrioJob), - self.claimer.get_greeter_sas, + self.get_greeter_sas_success, self.get_greeter_sas_error, self.claimer.get_greeter_sas ) def _on_good_greeter_code_clicked(self): self.widget_greeter_code.setDisabled(True) self.signify_trust_job = self.jobs_ctx.submit_job( - ThreadSafeQtSignal(self, "signify_trust_success", QtToTrioJob), - ThreadSafeQtSignal(self, "signify_trust_error", QtToTrioJob), - self.claimer.signify_trust, + self.signify_trust_success, self.signify_trust_error, self.claimer.signify_trust ) def _on_wrong_greeter_code_clicked(self): @@ -313,9 +309,7 @@ def _on_get_claimer_sas_success(self): self.widget_claimer_code.setVisible(True) self.line_edit_claimer_code.setText(str(claimer_sas)) self.wait_peer_trust_job = self.jobs_ctx.submit_job( - ThreadSafeQtSignal(self, "wait_peer_trust_success", QtToTrioJob), - ThreadSafeQtSignal(self, "wait_peer_trust_error", QtToTrioJob), - self.claimer.wait_peer_trust, + self.wait_peer_trust_success, self.wait_peer_trust_error, self.claimer.wait_peer_trust ) def _on_get_claimer_sas_error(self, job): @@ -333,9 +327,7 @@ def _on_signify_trust_success(self): assert self.signify_trust_job.status == "ok" self.signify_trust_job = None self.get_claimer_sas_job = self.jobs_ctx.submit_job( - ThreadSafeQtSignal(self, "get_claimer_sas_success", QtToTrioJob), - ThreadSafeQtSignal(self, "get_claimer_sas_error", QtToTrioJob), - self.claimer.get_claimer_sas, + self.get_claimer_sas_success, self.get_claimer_sas_error, self.claimer.get_claimer_sas ) def _on_signify_trust_error(self, job): @@ -450,8 +442,8 @@ def _on_claim_clicked(self): self.widget_info.setDisabled(True) self.label_wait.show() self.claim_job = self.jobs_ctx.submit_job( - ThreadSafeQtSignal(self, "claim_success", QtToTrioJob), - ThreadSafeQtSignal(self, "claim_error", QtToTrioJob), + self.claim_success, + self.claim_error, self.claimer.claim_user, device_label=device_label, human_handle=human_handle, @@ -516,9 +508,7 @@ def _on_button_start_clicked(self): self.button_start.setDisabled(True) self.button_start.setText(_("TEXT_CLAIM_USER_WAITING")) self.wait_peer_job = self.jobs_ctx.submit_job( - ThreadSafeQtSignal(self, "wait_peer_success", QtToTrioJob), - ThreadSafeQtSignal(self, "wait_peer_error", QtToTrioJob), - self.claimer.wait_peer, + self.wait_peer_success, self.wait_peer_error, self.claimer.wait_peer ) def _on_wait_peer_success(self, job): @@ -582,16 +572,14 @@ def __init__(self, jobs_ctx, config, addr): def _run_claimer(self): self.claimer_job = self.jobs_ctx.submit_job( - ThreadSafeQtSignal(self, "claimer_success", QtToTrioJob), - ThreadSafeQtSignal(self, "claimer_error", QtToTrioJob), + self.claimer_success, + self.claimer_error, self.claimer.run, addr=self.addr, config=self.config, ) self.retrieve_info_job = self.jobs_ctx.submit_job( - ThreadSafeQtSignal(self, "retrieve_info_success", QtToTrioJob), - ThreadSafeQtSignal(self, "retrieve_info_error", QtToTrioJob), - self.claimer.retrieve_info, + self.retrieve_info_success, self.retrieve_info_error, self.claimer.retrieve_info ) def _on_retrieve_info_success(self, job): diff --git a/parsec/core/gui/create_org_widget.py b/parsec/core/gui/create_org_widget.py index 60875fa2d72..4b863527512 100644 --- a/parsec/core/gui/create_org_widget.py +++ b/parsec/core/gui/create_org_widget.py @@ -22,7 +22,7 @@ from parsec.core.local_device import save_device_with_password from parsec.core.gui.custom_dialogs import GreyedDialog, show_error, show_info -from parsec.core.gui.trio_thread import JobResultError, ThreadSafeQtSignal +from parsec.core.gui.trio_thread import JobResultError from parsec.core.gui.desktop import get_default_device from parsec.core.gui.lang import translate as _ from parsec.core.gui import validators @@ -285,8 +285,8 @@ def _on_validate_clicked(self): return self.create_job = self.jobs_ctx.submit_job( - ThreadSafeQtSignal(self, "req_success"), - ThreadSafeQtSignal(self, "req_error"), + self.req_success, + self.req_error, _do_create_org, config=self.config, human_handle=human_handle, diff --git a/parsec/core/gui/devices_widget.py b/parsec/core/gui/devices_widget.py index 5fea76d34af..a13a455f8aa 100644 --- a/parsec/core/gui/devices_widget.py +++ b/parsec/core/gui/devices_widget.py @@ -5,7 +5,7 @@ from PyQt5.QtGui import QColor from parsec.core.backend_connection import BackendNotAvailable, BackendConnectionError -from parsec.core.gui.trio_thread import JobResultError, ThreadSafeQtSignal, QtToTrioJob +from parsec.core.gui.trio_thread import JobResultError, QtToTrioJob from parsec.core.gui.greet_device_widget import GreetDeviceWidget from parsec.core.gui.lang import translate as _ from parsec.core.gui.custom_widgets import ensure_string_size @@ -84,10 +84,7 @@ def show(self): def invite_device(self): self.jobs_ctx.submit_job( - ThreadSafeQtSignal(self, "invite_success", QtToTrioJob), - ThreadSafeQtSignal(self, "invite_error", QtToTrioJob), - _do_invite_device, - core=self.core, + self.invite_success, self.invite_error, _do_invite_device, core=self.core ) def _on_invite_success(self, job): @@ -146,8 +143,5 @@ def reset(self): self.layout_devices.clear() self.spinner.show() self.jobs_ctx.submit_job( - ThreadSafeQtSignal(self, "list_success", QtToTrioJob), - ThreadSafeQtSignal(self, "list_error", QtToTrioJob), - _do_list_devices, - core=self.core, + self.list_success, self.list_error, _do_list_devices, core=self.core ) diff --git a/parsec/core/gui/file_history_widget.py b/parsec/core/gui/file_history_widget.py index 522c086966d..c3933dab6d4 100644 --- a/parsec/core/gui/file_history_widget.py +++ b/parsec/core/gui/file_history_widget.py @@ -4,7 +4,6 @@ from PyQt5.QtWidgets import QWidget from parsec.core.gui.lang import translate as _, format_datetime from parsec.core.gui.custom_dialogs import show_error, GreyedDialog -from parsec.core.gui.trio_thread import ThreadSafeQtSignal from parsec.core.gui.file_size import get_filesize from parsec.core.gui.ui.file_history_widget import Ui_FileHistoryWidget from parsec.core.gui.ui.file_history_button import Ui_FileHistoryButton @@ -101,8 +100,8 @@ def reset_list(self): w.hide() w.setParent(0) self.versions_job = self.jobs_ctx.submit_job( - ThreadSafeQtSignal(self, "get_versions_success"), - ThreadSafeQtSignal(self, "get_versions_error"), + self.get_versions_success, + self.get_versions_error, _do_workspace_version, version_lister=self.version_lister, path=self.path, diff --git a/parsec/core/gui/files_widget.py b/parsec/core/gui/files_widget.py index 20781fbbfb6..7a2bdadfb18 100644 --- a/parsec/core/gui/files_widget.py +++ b/parsec/core/gui/files_widget.py @@ -18,7 +18,7 @@ FSFileNotFoundError, ) -from parsec.core.gui.trio_thread import JobResultError, ThreadSafeQtSignal, QtToTrioJob +from parsec.core.gui.trio_thread import JobResultError, QtToTrioJob from parsec.core.gui import desktop from parsec.core.gui.file_items import FileType, TYPE_DATA_INDEX, UUID_DATA_INDEX from parsec.core.gui.custom_dialogs import ( @@ -446,8 +446,8 @@ def on_paste_clicked(self): if self.clipboard.status == Clipboard.Status.Cut: self.jobs_ctx.submit_job( - ThreadSafeQtSignal(self, "move_success", QtToTrioJob), - ThreadSafeQtSignal(self, "move_error", QtToTrioJob), + self.move_success, + self.move_error, _do_move_files, workspace_fs=self.workspace_fs, target_dir=self.current_directory, @@ -461,8 +461,8 @@ def on_paste_clicked(self): elif self.clipboard.status == Clipboard.Status.Copied: self.jobs_ctx.submit_job( - ThreadSafeQtSignal(self, "copy_success", QtToTrioJob), - ThreadSafeQtSignal(self, "copy_error", QtToTrioJob), + self.copy_success, + self.copy_error, _do_copy_files, workspace_fs=self.workspace_fs, target_dir=self.current_directory, @@ -522,8 +522,8 @@ def rename_files(self): if not new_name: return self.jobs_ctx.submit_job( - ThreadSafeQtSignal(self, "rename_success", QtToTrioJob), - ThreadSafeQtSignal(self, "rename_error", QtToTrioJob), + self.rename_success, + self.rename_error, _do_rename, workspace_fs=self.workspace_fs, paths=[ @@ -546,8 +546,8 @@ def rename_files(self): return self.jobs_ctx.submit_job( - ThreadSafeQtSignal(self, "rename_success", QtToTrioJob), - ThreadSafeQtSignal(self, "rename_error", QtToTrioJob), + self.rename_success, + self.rename_error, _do_rename, workspace_fs=self.workspace_fs, paths=[ @@ -580,8 +580,8 @@ def delete_files(self): if result != _("ACTION_FILE_DELETE_MULTIPLE") and result != _("ACTION_FILE_DELETE"): return self.jobs_ctx.submit_job( - ThreadSafeQtSignal(self, "delete_success", QtToTrioJob), - ThreadSafeQtSignal(self, "delete_error", QtToTrioJob), + self.delete_success, + self.delete_error, _do_delete, workspace_fs=self.workspace_fs, files=[(self.current_directory / f.name, f.type) for f in files], @@ -640,8 +640,8 @@ def reload(self, default_selection=None, delay=RELOAD_FILES_LIST_THROTTLE_DELAY) self.jobs_ctx.submit_throttled_job( "files_widget.reload", delay, - ThreadSafeQtSignal(self, "folder_stat_success", QtToTrioJob), - ThreadSafeQtSignal(self, "folder_stat_error", QtToTrioJob), + self.folder_stat_success, + self.folder_stat_error, _do_folder_stat, workspace_fs=self.workspace_fs, path=self.current_directory, @@ -652,8 +652,8 @@ def reload(self, default_selection=None, delay=RELOAD_FILES_LIST_THROTTLE_DELAY) def load(self, directory, default_selection=None): self.spinner.show() self.jobs_ctx.submit_job( - ThreadSafeQtSignal(self, "folder_stat_success", QtToTrioJob), - ThreadSafeQtSignal(self, "folder_stat_error", QtToTrioJob), + self.folder_stat_success, + self.folder_stat_error, _do_folder_stat, workspace_fs=self.workspace_fs, path=directory, @@ -670,13 +670,13 @@ def import_all(self, files, total_size): self.loading_dialog.show() self.import_job = self.jobs_ctx.submit_job( - ThreadSafeQtSignal(self, "import_success", QtToTrioJob), - ThreadSafeQtSignal(self, "import_error", QtToTrioJob), + self.import_success, + self.import_error, _do_import, workspace_fs=self.workspace_fs, files=files, total_size=total_size, - progress_signal=ThreadSafeQtSignal(self, "import_progress", str, int), + progress_signal=self.import_progress, ) def cancel_import(self): @@ -774,8 +774,8 @@ def on_table_files_file_moved(self, file_type, file_name, target_name): else: target_dir = self.current_directory / target_name self.jobs_ctx.submit_job( - ThreadSafeQtSignal(self, "move_success", QtToTrioJob), - ThreadSafeQtSignal(self, "move_error", QtToTrioJob), + self.move_success, + self.move_error, _do_move_files, workspace_fs=self.workspace_fs, target_dir=target_dir, @@ -806,8 +806,8 @@ def create_folder_clicked(self): return self.jobs_ctx.submit_job( - ThreadSafeQtSignal(self, "folder_create_success", QtToTrioJob), - ThreadSafeQtSignal(self, "folder_create_error", QtToTrioJob), + self.folder_create_success, + self.folder_create_error, _do_folder_create, workspace_fs=self.workspace_fs, path=self.current_directory / folder_name, @@ -963,8 +963,8 @@ def _display_import_error(file_count, exceptions=None): if hasattr(self.import_job.exc, "status") and self.import_job.exc.status == "cancelled": self.jobs_ctx.submit_job( - ThreadSafeQtSignal(self, "delete_success", QtToTrioJob), - ThreadSafeQtSignal(self, "delete_error", QtToTrioJob), + self.delete_success, + self.delete_error, _do_delete, workspace_fs=self.workspace_fs, files=[(self.import_job.exc.params["last_file"], FileType.File)], @@ -1050,8 +1050,8 @@ def _on_reload_timestamped_requested( self, timestamp, path, file_type, open_after_load, close_after_remount, reload_after_remount ): self.jobs_ctx.submit_job( - ThreadSafeQtSignal(self, "reload_timestamped_success", QtToTrioJob), - ThreadSafeQtSignal(self, "reload_timestamped_error", QtToTrioJob), + self.reload_timestamped_success, + self.reload_timestamped_error, _do_remount_timestamped, mountpoint_manager=self.core.mountpoint_manager, workspace_fs=self.workspace_fs, diff --git a/parsec/core/gui/greet_device_widget.py b/parsec/core/gui/greet_device_widget.py index 01b5d2e5174..edc38829268 100644 --- a/parsec/core/gui/greet_device_widget.py +++ b/parsec/core/gui/greet_device_widget.py @@ -8,7 +8,7 @@ from parsec.core.backend_connection import BackendNotAvailable, BackendConnectionError from parsec.core.invite import InviteError, InvitePeerResetError, InviteAlreadyUsedError -from parsec.core.gui.trio_thread import JobResultError, ThreadSafeQtSignal, QtToTrioJob +from parsec.core.gui.trio_thread import JobResultError, QtToTrioJob from parsec.core.gui.custom_dialogs import show_error, GreyedDialog, show_info from parsec.core.gui.lang import translate as _ from parsec.core.gui.qrcode_widget import generate_qr_code @@ -169,10 +169,7 @@ def _on_copy_addr_clicked(self): def _on_button_send_email_clicked(self): self.button_send_email.setDisabled(True) self.jobs_ctx.submit_job( - ThreadSafeQtSignal(self, "send_email_success", QtToTrioJob), - ThreadSafeQtSignal(self, "send_email_error", QtToTrioJob), - _do_send_email, - core=self.core, + self.send_email_success, self.send_email_error, _do_send_email, core=self.core ) def _on_send_email_success(self, job): @@ -190,9 +187,7 @@ def _on_button_start_clicked(self): self.button_send_email.setDisabled(True) self.button_start.setText(_("TEXT_GREET_DEVICE_WAITING")) self.wait_peer_job = self.jobs_ctx.submit_job( - ThreadSafeQtSignal(self, "wait_peer_success", QtToTrioJob), - ThreadSafeQtSignal(self, "wait_peer_error", QtToTrioJob), - self.greeter.wait_peer, + self.wait_peer_success, self.wait_peer_error, self.greeter.wait_peer ) def _on_wait_peer_success(self, job): @@ -273,18 +268,14 @@ def __init__(self, jobs_ctx, greeter): self.label_wait_info.hide() self.get_greeter_sas_job = self.jobs_ctx.submit_job( - ThreadSafeQtSignal(self, "get_greeter_sas_success", QtToTrioJob), - ThreadSafeQtSignal(self, "get_greeter_sas_error", QtToTrioJob), - self.greeter.get_greeter_sas, + self.get_greeter_sas_success, self.get_greeter_sas_error, self.greeter.get_greeter_sas ) def _on_good_claimer_code_clicked(self): self.widget_claimer_code.hide() self.label_wait_info.show() self.signify_trust_job = self.jobs_ctx.submit_job( - ThreadSafeQtSignal(self, "signify_trust_success", QtToTrioJob), - ThreadSafeQtSignal(self, "signify_trust_error", QtToTrioJob), - self.greeter.signify_trust, + self.signify_trust_success, self.signify_trust_error, self.greeter.signify_trust ) def _on_wrong_claimer_code_clicked(self): @@ -305,9 +296,7 @@ def _on_get_greeter_sas_success(self, job): greeter_sas = job.ret self.line_edit_greeter_code.setText(str(greeter_sas)) self.wait_peer_trust_job = self.jobs_ctx.submit_job( - ThreadSafeQtSignal(self, "wait_peer_trust_success", QtToTrioJob), - ThreadSafeQtSignal(self, "wait_peer_trust_error", QtToTrioJob), - self.greeter.wait_peer_trust, + self.wait_peer_trust_success, self.wait_peer_trust_error, self.greeter.wait_peer_trust ) def _on_get_greeter_sas_error(self, job): @@ -390,9 +379,7 @@ def _on_wait_peer_trust_success(self, job): assert job.is_finished() assert job.status == "ok" self.get_claimer_sas_job = self.jobs_ctx.submit_job( - ThreadSafeQtSignal(self, "get_claimer_sas_success", QtToTrioJob), - ThreadSafeQtSignal(self, "get_claimer_sas_error", QtToTrioJob), - self.greeter.get_claimer_sas, + self.get_claimer_sas_success, self.get_claimer_sas_error, self.greeter.get_claimer_sas ) def _on_wait_peer_trust_error(self, job): @@ -432,8 +419,8 @@ def __init__(self, core, jobs_ctx, invite_addr): def _run_greeter(self): self.greeter_job = self.jobs_ctx.submit_job( - ThreadSafeQtSignal(self, "greeter_success", QtToTrioJob), - ThreadSafeQtSignal(self, "greeter_error", QtToTrioJob), + self.greeter_success, + self.greeter_error, self.greeter.run, core=self.core, token=self.invite_addr.token, diff --git a/parsec/core/gui/greet_user_widget.py b/parsec/core/gui/greet_user_widget.py index 22f5f71f60e..4754b69013f 100644 --- a/parsec/core/gui/greet_user_widget.py +++ b/parsec/core/gui/greet_user_widget.py @@ -10,7 +10,7 @@ from parsec.api.protocol import HumanHandle from parsec.core.backend_connection import BackendNotAvailable from parsec.core.invite import InviteError, InvitePeerResetError, InviteAlreadyUsedError -from parsec.core.gui.trio_thread import JobResultError, ThreadSafeQtSignal, QtToTrioJob +from parsec.core.gui.trio_thread import JobResultError, QtToTrioJob from parsec.core.gui.custom_dialogs import show_error, GreyedDialog, show_info from parsec.core.gui import validators from parsec.core.gui.lang import translate as _ @@ -179,9 +179,7 @@ def _on_button_start_clicked(self, checked): self.button_start.setDisabled(True) self.button_start.setText(_("TEXT_GREET_USER_WAITING")) self.wait_peer_job = self.jobs_ctx.submit_job( - ThreadSafeQtSignal(self, "wait_peer_success", QtToTrioJob), - ThreadSafeQtSignal(self, "wait_peer_error", QtToTrioJob), - self.greeter.wait_peer, + self.wait_peer_success, self.wait_peer_error, self.greeter.wait_peer ) def _on_wait_peer_success(self, job): @@ -258,9 +256,7 @@ def __init__(self, jobs_ctx, greeter, user_profile_outsider_allowed=False): self.button_create_user.clicked.connect(self._on_create_user_clicked) self.get_requests_job = self.jobs_ctx.submit_job( - ThreadSafeQtSignal(self, "get_requests_success", QtToTrioJob), - ThreadSafeQtSignal(self, "get_requests_error", QtToTrioJob), - self.greeter.get_claim_requests, + self.get_requests_success, self.get_requests_error, self.greeter.get_claim_requests ) def check_infos(self, _=None): @@ -286,8 +282,8 @@ def _on_create_user_clicked(self): self.button_create_user.setDisabled(True) self.button_create_user.setText(_("TEXT_GREET_USER_WAITING")) self.create_user_job = self.jobs_ctx.submit_job( - ThreadSafeQtSignal(self, "create_user_success", QtToTrioJob), - ThreadSafeQtSignal(self, "create_user_error", QtToTrioJob), + self.create_user_success, + self.create_user_error, self.greeter.create_new_user, human_handle=handle, device_label=device_label, @@ -401,17 +397,13 @@ def __init__(self, jobs_ctx, greeter): self.get_claimer_sas_error.connect(self._on_get_claimer_sas_error) self.get_greeter_sas_job = self.jobs_ctx.submit_job( - ThreadSafeQtSignal(self, "get_greeter_sas_success", QtToTrioJob), - ThreadSafeQtSignal(self, "get_greeter_sas_error", QtToTrioJob), - self.greeter.get_greeter_sas, + self.get_greeter_sas_success, self.get_greeter_sas_error, self.greeter.get_greeter_sas ) def _on_good_claimer_code_clicked(self): self.widget_claimer_code.setDisabled(True) self.signify_trust_job = self.jobs_ctx.submit_job( - ThreadSafeQtSignal(self, "signify_trust_success", QtToTrioJob), - ThreadSafeQtSignal(self, "signify_trust_error", QtToTrioJob), - self.greeter.signify_trust, + self.signify_trust_success, self.signify_trust_error, self.greeter.signify_trust ) def _on_wrong_claimer_code_clicked(self): @@ -432,9 +424,7 @@ def _on_get_greeter_sas_success(self, job): greeter_sas = job.ret self.line_edit_greeter_code.setText(str(greeter_sas)) self.wait_peer_trust_job = self.jobs_ctx.submit_job( - ThreadSafeQtSignal(self, "wait_peer_trust_success", QtToTrioJob), - ThreadSafeQtSignal(self, "wait_peer_trust_error", QtToTrioJob), - self.greeter.wait_peer_trust, + self.wait_peer_trust_success, self.wait_peer_trust_error, self.greeter.wait_peer_trust ) def _on_get_greeter_sas_error(self, job): @@ -517,9 +507,7 @@ def _on_wait_peer_trust_success(self, job): assert job.is_finished() assert job.status == "ok" self.get_claimer_sas_job = self.jobs_ctx.submit_job( - ThreadSafeQtSignal(self, "get_claimer_sas_success", QtToTrioJob), - ThreadSafeQtSignal(self, "get_claimer_sas_error", QtToTrioJob), - self.greeter.get_claimer_sas, + self.get_claimer_sas_success, self.get_claimer_sas_error, self.greeter.get_claimer_sas ) def _on_wait_peer_trust_error(self, job): @@ -559,8 +547,8 @@ def __init__(self, core, jobs_ctx, token): def _run_greeter(self): self.greeter_job = self.jobs_ctx.submit_job( - ThreadSafeQtSignal(self, "greeter_success", QtToTrioJob), - ThreadSafeQtSignal(self, "greeter_error", QtToTrioJob), + self.greeter_success, + self.greeter_error, self.greeter.run, core=self.core, token=self.token, diff --git a/parsec/core/gui/instance_widget.py b/parsec/core/gui/instance_widget.py index 19929d801f0..9ffd7c385af 100644 --- a/parsec/core/gui/instance_widget.py +++ b/parsec/core/gui/instance_widget.py @@ -19,11 +19,7 @@ MountpointWinfspNotAvailable, ) -from parsec.core.gui.trio_thread import ( - QtToTrioJobScheduler, - ThreadSafeQtSignal, - run_trio_job_scheduler, -) +from parsec.core.gui.trio_thread import QtToTrioJobScheduler, run_trio_job_scheduler from parsec.core.gui.parsec_application import ParsecApp from parsec.core.gui.custom_dialogs import show_error, show_info_link from parsec.core.gui.lang import translate as _ @@ -130,12 +126,12 @@ def start_core(self, device): self.config = ParsecApp.get_main_window().config self.running_core_job = self.jobs_ctx.submit_job( - ThreadSafeQtSignal(self, "run_core_success"), - ThreadSafeQtSignal(self, "run_core_error"), + self.run_core_success, + self.run_core_error, _do_run_core, self.config, device, - ThreadSafeQtSignal(self, "run_core_ready", object, object), + self.run_core_ready, ) def on_run_core_ready(self, core, core_jobs_ctx): diff --git a/parsec/core/gui/new_version.py b/parsec/core/gui/new_version.py index 2bd58520c20..d77480cfd68 100644 --- a/parsec/core/gui/new_version.py +++ b/parsec/core/gui/new_version.py @@ -16,7 +16,6 @@ from parsec import __version__ from parsec.serde import BaseSchema, fields, JSONSerializer, SerdeError from parsec.core.gui import desktop -from parsec.core.gui.trio_thread import ThreadSafeQtSignal from parsec.core.gui.lang import translate as _ from parsec.core.gui.ui.new_version_dialog import Ui_NewVersionDialog from parsec.core.gui.ui.new_version_info import Ui_NewVersionInfo @@ -204,8 +203,8 @@ def __init__(self, jobs_ctx, event_bus, config, **kwargs): self.check_new_version_error.connect(self.on_check_new_version_error) self.version_job = self.jobs_ctx.submit_job( - ThreadSafeQtSignal(self, "check_new_version_success"), - ThreadSafeQtSignal(self, "check_new_version_error"), + self.check_new_version_success, + self.check_new_version_error, do_check_new_version, api_url=self.config.gui_check_version_api_url, allow_prerelease=self.config.gui_check_version_allow_pre_release, diff --git a/parsec/core/gui/timestamped_workspace_widget.py b/parsec/core/gui/timestamped_workspace_widget.py index 0c7cf472c9f..ef024886287 100644 --- a/parsec/core/gui/timestamped_workspace_widget.py +++ b/parsec/core/gui/timestamped_workspace_widget.py @@ -10,7 +10,6 @@ from parsec.core.gui.lang import get_qlocale, translate as _, format_datetime from parsec.core.gui.custom_dialogs import show_error, GreyedDialog -from parsec.core.gui.trio_thread import ThreadSafeQtSignal from parsec.core.gui.ui.timestamped_workspace_widget import Ui_TimestampedWorkspaceWidget @@ -40,8 +39,8 @@ def __init__(self, workspace_fs, jobs_ctx): self.get_creation_timestamp_success.connect(self.enable_with_timestamp) self.get_creation_timestamp_error.connect(self.on_error) self.limits_job = self.jobs_ctx.submit_job( - ThreadSafeQtSignal(self, "get_creation_timestamp_success"), - ThreadSafeQtSignal(self, "get_creation_timestamp_error"), + self.get_creation_timestamp_success, + self.get_creation_timestamp_error, _do_workspace_get_creation_timestamp, workspace_fs=workspace_fs, ) diff --git a/parsec/core/gui/trio_thread.py b/parsec/core/gui/trio_thread.py index 357520dde39..9a3f15cb5da 100644 --- a/parsec/core/gui/trio_thread.py +++ b/parsec/core/gui/trio_thread.py @@ -9,7 +9,6 @@ from parsec.core.fs import FSError from parsec.core.mountpoint import MountpointError from parsec.utils import split_multi_error -from PyQt5.QtCore import pyqtBoundSignal, Q_ARG, QMetaObject, Qt logger = get_logger() @@ -26,32 +25,8 @@ class JobSchedulerNotAvailable(Exception): pass -class ThreadSafeQtSignal: - def __init__(self, qobj, signal_name, *args_types): - signal = getattr(qobj, signal_name) - assert isinstance(signal, pyqtBoundSignal) - self.qobj = qobj - self.signal_name = signal_name - self.args_types = args_types - - def emit(self, *args): - assert len(self.args_types) == len(args) - cooked_args = [Q_ARG(t, v) for t, v in zip(self.args_types, args)] - QMetaObject.invokeMethod(self.qobj, self.signal_name, Qt.QueuedConnection, *cooked_args) - - def __repr__(self): - return f"" - - class QtToTrioJob: def __init__(self, fn, args, kwargs, qt_on_success, qt_on_error): - # Fool-proof sanity check, signals must be wrapped in `ThreadSafeQtSignal` - assert not [x for x in args if isinstance(x, pyqtBoundSignal)] - assert not [v for v in kwargs.values() if isinstance(v, pyqtBoundSignal)] - assert isinstance(qt_on_success, ThreadSafeQtSignal) - assert qt_on_success.args_types in ((), (QtToTrioJob,)) - assert isinstance(qt_on_error, ThreadSafeQtSignal) - assert qt_on_error.args_types in ((), (QtToTrioJob,)) self._qt_on_success = qt_on_success self._qt_on_error = qt_on_error self._fn = fn diff --git a/parsec/core/gui/users_widget.py b/parsec/core/gui/users_widget.py index fe3ce7ae876..91185a52f54 100644 --- a/parsec/core/gui/users_widget.py +++ b/parsec/core/gui/users_widget.py @@ -18,7 +18,7 @@ BackendInvitationOnExistingMember, ) -from parsec.core.gui.trio_thread import JobResultError, ThreadSafeQtSignal, QtToTrioJob +from parsec.core.gui.trio_thread import JobResultError, QtToTrioJob from parsec.core.gui.custom_dialogs import show_error, show_info, ask_question, get_text_input from parsec.core.gui.custom_widgets import ensure_string_size from parsec.core.gui.flow_layout import FlowLayout @@ -274,8 +274,8 @@ def on_filter(self, editing_finished=False, text_changed=False, change_page=Fals self.button_users_filter.setEnabled(False) self.line_edit_search.setEnabled(False) self.jobs_ctx.submit_job( - ThreadSafeQtSignal(self, "list_success", QtToTrioJob), - ThreadSafeQtSignal(self, "list_error", QtToTrioJob), + self.list_success, + self.list_error, _do_list_users_and_invitations, core=self.core, page=self._page, @@ -295,8 +295,8 @@ def invite_user(self): return self.jobs_ctx.submit_job( - ThreadSafeQtSignal(self, "invite_user_success", QtToTrioJob), - ThreadSafeQtSignal(self, "invite_user_error", QtToTrioJob), + self.invite_user_success, + self.invite_user_error, _do_invite_user, core=self.core, email=user_email, @@ -335,8 +335,8 @@ def cancel_invitation(self, token): if r != _("TEXT_USER_INVITE_CANCEL_INVITE_ACCEPT"): return self.jobs_ctx.submit_job( - ThreadSafeQtSignal(self, "cancel_invitation_success", QtToTrioJob), - ThreadSafeQtSignal(self, "cancel_invitation_error", QtToTrioJob), + self.cancel_invitation_success, + self.cancel_invitation_error, _do_cancel_invitation, core=self.core, token=token, @@ -390,8 +390,8 @@ def revoke_user(self, user_info): if result != _("ACTION_USER_REVOCATION_CONFIRM"): return self.jobs_ctx.submit_job( - ThreadSafeQtSignal(self, "revoke_success", QtToTrioJob), - ThreadSafeQtSignal(self, "revoke_error", QtToTrioJob), + self.revoke_success, + self.revoke_error, _do_revoke_user, core=self.core, user_info=user_info, @@ -524,8 +524,8 @@ def reset(self): self.button_next_page.hide() self.spinner.show() self.jobs_ctx.submit_job( - ThreadSafeQtSignal(self, "list_success", QtToTrioJob), - ThreadSafeQtSignal(self, "list_error", QtToTrioJob), + self.list_success, + self.list_error, _do_list_users_and_invitations, core=self.core, page=self._page, diff --git a/parsec/core/gui/workspace_sharing_widget.py b/parsec/core/gui/workspace_sharing_widget.py index 5aef752b540..6ae918e6f45 100644 --- a/parsec/core/gui/workspace_sharing_widget.py +++ b/parsec/core/gui/workspace_sharing_widget.py @@ -11,7 +11,7 @@ from parsec.core.types import WorkspaceRole from parsec.core.backend_connection import BackendNotAvailable -from parsec.core.gui.trio_thread import JobResultError, ThreadSafeQtSignal, QtToTrioJob +from parsec.core.gui.trio_thread import JobResultError, QtToTrioJob from parsec.core.gui.custom_dialogs import show_error, GreyedDialog from parsec.core.gui.custom_widgets import Pixmap @@ -234,8 +234,8 @@ def on_role_changed(self, user_info, role): if sharing_widget: sharing_widget.set_status_updating() self.jobs_ctx.submit_job( - ThreadSafeQtSignal(self, "share_success", QtToTrioJob), - ThreadSafeQtSignal(self, "share_error", QtToTrioJob), + self.share_success, + self.share_error, _do_share_workspace, user_fs=self.user_fs, workspace_fs=self.workspace_fs, @@ -319,8 +319,8 @@ def reset(self): self.spinner.show() self.widget_users.hide() self.jobs_ctx.submit_job( - ThreadSafeQtSignal(self, "get_users_success", QtToTrioJob), - ThreadSafeQtSignal(self, "get_users_error", QtToTrioJob), + self.get_users_success, + self.get_users_error, _do_get_users, core=self.core, workspace_fs=self.workspace_fs, diff --git a/parsec/core/gui/workspaces_widget.py b/parsec/core/gui/workspaces_widget.py index 426411e0ffc..1b2e99e8a48 100644 --- a/parsec/core/gui/workspaces_widget.py +++ b/parsec/core/gui/workspaces_widget.py @@ -36,12 +36,7 @@ MountpointNoDriveAvailable, ) -from parsec.core.gui.trio_thread import ( - JobResultError, - ThreadSafeQtSignal, - QtToTrioJob, - JobSchedulerNotAvailable, -) +from parsec.core.gui.trio_thread import JobResultError, QtToTrioJob, JobSchedulerNotAvailable from parsec.core.gui import desktop from parsec.core.gui.custom_dialogs import show_error, get_text_input, ask_question from parsec.core.gui.flow_layout import FlowLayout @@ -398,8 +393,8 @@ def on_list_success(self, job): if button.is_owner: try: self.jobs_ctx.submit_job( - ThreadSafeQtSignal(self, "reencryption_needs_success", QtToTrioJob), - ThreadSafeQtSignal(self, "reencryption_needs_error", QtToTrioJob), + self.reencryption_needs_success, + self.reencryption_needs_error, _get_reencryption_needs, workspace_fs=workspace_fs, ) @@ -502,8 +497,8 @@ def fix_legacy_workspace_names(self, workspace_fs, workspace_name): if token in workspace_name: workspace_name, *_ = workspace_name.split(token) self.jobs_ctx.submit_job( - ThreadSafeQtSignal(self, "ignore_success", QtToTrioJob), - ThreadSafeQtSignal(self, "ignore_error", QtToTrioJob), + self.ignore_success, + self.ignore_error, _do_workspace_rename, core=self.core, workspace_id=workspace_fs.workspace_id, @@ -559,8 +554,8 @@ def _on_finished(date, time): def mount_workspace(self, workspace_id, timestamp=None): self.jobs_ctx.submit_job( - ThreadSafeQtSignal(self, "mount_success", QtToTrioJob), - ThreadSafeQtSignal(self, "mount_error", QtToTrioJob), + self.mount_success, + self.mount_error, _do_workspace_mount, core=self.core, workspace_id=workspace_id, @@ -569,8 +564,8 @@ def mount_workspace(self, workspace_id, timestamp=None): def unmount_workspace(self, workspace_id, timestamp=None): self.jobs_ctx.submit_job( - ThreadSafeQtSignal(self, "unmount_success", QtToTrioJob), - ThreadSafeQtSignal(self, "unmount_error", QtToTrioJob), + self.unmount_success, + self.unmount_error, _do_workspace_unmount, core=self.core, workspace_id=workspace_id, @@ -620,8 +615,8 @@ def rename_workspace(self, workspace_button): if not new_name: return self.jobs_ctx.submit_job( - ThreadSafeQtSignal(self, "rename_success", QtToTrioJob), - ThreadSafeQtSignal(self, "rename_error", QtToTrioJob), + self.rename_success, + self.rename_error, _do_workspace_rename, core=self.core, workspace_id=workspace_button.workspace_fs.workspace_id, @@ -701,12 +696,10 @@ async def _reencrypt(on_progress, workspace_id): workspace_button.reencrypting = 1, 0 self.jobs_ctx.submit_job( - ThreadSafeQtSignal(self, "workspace_reencryption_success", QtToTrioJob), - ThreadSafeQtSignal(self, "workspace_reencryption_error", QtToTrioJob), + self.workspace_reencryption_success, + self.workspace_reencryption_error, _reencrypt, - on_progress=ThreadSafeQtSignal( - self, "workspace_reencryption_progress", EntryID, int, int - ), + on_progress=self.workspace_reencryption_progress, workspace_id=workspace_id, ) @@ -759,8 +752,8 @@ def create_workspace_clicked(self): if not workspace_name: return self.jobs_ctx.submit_job( - ThreadSafeQtSignal(self, "create_success", QtToTrioJob), - ThreadSafeQtSignal(self, "create_error", QtToTrioJob), + self.create_success, + self.create_error, _do_workspace_create, core=self.core, workspace_name=workspace_name, @@ -776,8 +769,8 @@ def list_workspaces(self): self.jobs_ctx.submit_throttled_job( "workspace_widget.list_workspaces", self.REFRESH_WORKSPACES_LIST_DELAY, - ThreadSafeQtSignal(self, "list_success", QtToTrioJob), - ThreadSafeQtSignal(self, "list_error", QtToTrioJob), + self.list_success, + self.list_error, _do_workspace_list, core=self.core, ) From 9b3933beecd45e1226df95c0b5597dae801e1052 Mon Sep 17 00:00:00 2001 From: Vincent Michel Date: Mon, 5 Jul 2021 19:43:35 +0200 Subject: [PATCH 03/35] Migrate GUI tests to qtrio --- parsec/core/gui/instance_widget.py | 2 +- parsec/core/gui/trio_thread.py | 12 +- tests/core/gui/conftest.py | 323 ++++++++++------------------- tests/core/gui/test_files.py | 8 +- tests/core/gui/test_login.py | 7 +- tests/core/gui/test_trio_thread.py | 51 ----- 6 files changed, 130 insertions(+), 273 deletions(-) delete mode 100644 tests/core/gui/test_trio_thread.py diff --git a/parsec/core/gui/instance_widget.py b/parsec/core/gui/instance_widget.py index 9ffd7c385af..09087332e50 100644 --- a/parsec/core/gui/instance_widget.py +++ b/parsec/core/gui/instance_widget.py @@ -192,7 +192,7 @@ def on_core_run_done(self): def stop_core(self): if self.running_core_job: - self.running_core_job.cancel_and_join() + self.running_core_job.cancel() def on_logged_out(self): self.state_changed.emit(self, "logout") diff --git a/parsec/core/gui/trio_thread.py b/parsec/core/gui/trio_thread.py index 9a3f15cb5da..9ad09de4cb8 100644 --- a/parsec/core/gui/trio_thread.py +++ b/parsec/core/gui/trio_thread.py @@ -120,7 +120,10 @@ def set_exception(self, exc): def _set_done(self): self._done.set() signal = self._qt_on_success if self.is_ok() else self._qt_on_error - signal.emit(self) if signal.args_types else signal.emit() + if signal.signal.endswith("(PyQt_PyObject)"): + signal.emit(self) + else: + signal.emit() def cancel(self): self.cancel_scope.cancel() @@ -179,12 +182,15 @@ async def _throttled_execute(task_status=trio.TASK_STATUS_IGNORED): self.nursery.start_soon(_throttled_execute) def submit_job(self, qt_on_success, qt_on_error, fn, *args, **kwargs): - assert not self.nursery._closed + if self.nursery._closed: + raise JobSchedulerNotAvailable job = QtToTrioJob(fn, args, kwargs, qt_on_success, qt_on_error) self.nursery.start_soon(job) + return job def run_sync(self, fn, *args): - assert not self.nursery._closed + if self.nursery_closed: + raise JobSchedulerNotAvailable return fn(*args) diff --git a/tests/core/gui/conftest.py b/tests/core/gui/conftest.py index 9d3c0435783..c41d51c7e73 100644 --- a/tests/core/gui/conftest.py +++ b/tests/core/gui/conftest.py @@ -1,14 +1,12 @@ # Parsec Cloud (https://parsec.cloud) Copyright (c) AGPLv3 2016-2021 Scille SAS -import pytest -from unittest.mock import patch -from functools import wraps, partial -import trio -from trio.testing import trio_test as vanilla_trio_test -import queue -import threading -from concurrent import futures +import time from importlib import import_module +from contextlib import asynccontextmanager + +import trio +import qtrio +import pytest from PyQt5 import QtCore from parsec import __version__ as parsec_version @@ -23,194 +21,104 @@ from parsec.core.local_device import LocalDeviceAlreadyExistsError -class ThreadedTrioTestRunner: - def __init__(self): - self._thread = None - self._trio_token = None - self._request_queue = queue.Queue() - self._test_result = futures.Future() - self._job_scheduler = QtToTrioJobScheduler() - - # State events - self._stopping = None - self._started = threading.Event() - - def start_test_thread(self, fn, **kwargs): - async_target = partial(self._test_target, fn) - sync_target = partial(vanilla_trio_test(async_target), **kwargs) - self._thread = threading.Thread(target=sync_target) - self._thread.start() - self._started.wait() - - def process_requests_until_test_result(self): - self._process_requests() - return self._test_result.result() - - def stop_test_thread(self): - # Set the stopping state event - trio.from_thread.run_sync(self._stopping.set, trio_token=self._trio_token) - self._thread.join() - - async def send_action(self, fn, *args, **kwargs): - reply_sender, reply_receiver = trio.open_memory_channel(1) - - def reply_callback(future): - trio.from_thread.run_sync(reply_sender.send_nowait, future, trio_token=self._trio_token) - - request = partial(fn, *args, **kwargs) - self._request_queue.put_nowait((request, reply_callback)) - reply = await reply_receiver.receive() - return reply.result() - - def _process_requests(self): - for request, callback in iter(self._request_queue.get, None): - reply = futures.Future() - try: - result = request() - except Exception as exc: - reply.set_exception(exc) - else: - reply.set_result(result) - callback(reply) - - async def _run_with_job_scheduler(self, fn, **kwargs): - async with trio.open_service_nursery() as nursery: - await nursery.start(self._job_scheduler._start) - try: - return await fn(**kwargs) - finally: - await self._job_scheduler._stop() - - async def _test_target(self, fn, **kwargs): - # Initialize trio objects - self._stopping = trio.Event() - self._trio_token = trio.lowlevel.current_trio_token() - - # Set the started state event - self._started.set() - - # Run the test - try: - result = await self._run_with_job_scheduler(fn, **kwargs) - except BaseException as exc: - self._test_result.set_exception(exc) - else: - self._test_result.set_result(result) - - # Indicate there will be no more requests - self._request_queue.put_nowait(None) - - # Let the trio loop run until teardown - await self._stopping.wait() - - -# Not an async fixture (and doesn't depend on an async fixture either) -# this fixture will actually be executed *before* pytest-trio setup -# the trio loop, giving us a chance to monkeypatch it ! @pytest.fixture -def run_trio_test_in_thread(): - runner = ThreadedTrioTestRunner() - - def trio_test(run): - def decorator(fn): - @wraps(fn) - def wrapper(**kwargs): - runner.start_test_thread(fn, **kwargs) - return runner.process_requests_until_test_result() - - return wrapper - - return decorator +@pytest.mark.trio +async def job_scheduler(): + async with trio.open_nursery() as nursery: + yield QtToTrioJobScheduler(nursery) + nursery.cancel_scope.cancel() - with patch("pytest_trio.plugin._trio_test", new=trio_test): - yield runner +@pytest.fixture +def aqtbot(qtbot): + return AsyncQtBot(qtbot) - # Wait for the last moment before stopping the thread - runner.stop_test_thread() +class AsyncQtBot: + def __init__(self, qtbot): + self.qtbot = qtbot -@pytest.fixture -async def qt_thread_gateway(run_trio_test_in_thread): - return run_trio_test_in_thread + async def run(self, fn, *args, **kwargs): + return fn(*args, **kwargs) + def __getattr__(self, name): + words = name.split("_") + camel_name = words[0] + "".join(word.title() for word in words[1:]) + if hasattr(self.qtbot, camel_name): -@pytest.fixture -async def aqtbot(qtbot, run_trio_test_in_thread): - return AsyncQtBot(qtbot, run_trio_test_in_thread) + async def method(*args, **kwargs): + return getattr(self.qtbot, camel_name)(*args, **kwargs) + return method -class CtxManagerAsyncWrapped: - def __init__(self, qtbot, qt_thread_gateway, fnname, *args, **kwargs): - self.qtbot = qtbot - self.qt_thread_gateway = qt_thread_gateway - self.fnname = fnname - self.args = args - self.kwargs = kwargs - self.ctx = None + raise AttributeError(name) - async def __aenter__(self): - def _action_enter(): - self.ctx = getattr(self.qtbot, self.fnname)(*self.args, **self.kwargs) - self.ctx.__enter__() + async def wait(self, timeout): + await trio.sleep(timeout / 1000) - await self.qt_thread_gateway.send_action(_action_enter) + async def wait_until(self, callback, *, timeout=5000): + """Implementation shamelessly adapted from: + https://github.com/pytest-dev/pytest-qt/blob/16b989d700dfb91fe389999d8e2676437169ed44/src/pytestqt/qtbot.py#L459 + """ + __tracebackhide__ = True - async def __aexit__(self, exc_type, exc, tb): - def _action_exit(): - self.ctx.__exit__(exc_type, exc, tb) + start = time.time() - await self.qt_thread_gateway.send_action(_action_exit) + def timed_out(): + elapsed = time.time() - start + elapsed_ms = elapsed * 1000 + return elapsed_ms > timeout + timeout_msg = f"wait_until timed out in {timeout} milliseconds" -class AsyncQtBot: - def __init__(self, qtbot, qt_thread_gateway): - self.qtbot = qtbot - self.qt_thread_gateway = qt_thread_gateway - self.run = self.qt_thread_gateway.send_action - - def _autowrap(fnname): - async def wrapper(*args, **kwargs): - def _action(): - return getattr(self.qtbot, fnname)(*args, **kwargs) - - return await self.qt_thread_gateway.send_action(_action) - - wrapper.__name__ = f"{fnname}" - return wrapper - - self.key_click = _autowrap("keyClick") - self.key_clicks = _autowrap("keyClicks") - self.key_event = _autowrap("keyEvent") - self.key_press = _autowrap("keyPress") - self.key_release = _autowrap("keyRelease") - # self.key_to_ascii = self.qtbot.keyToAscii # available ? - self.mouse_click = _autowrap("mouseClick") - self.mouse_d_click = _autowrap("mouseDClick") - self.mouse_move = _autowrap("mouseMove") - self.mouse_press = _autowrap("mousePress") - self.mouse_release = _autowrap("mouseRelease") - - self.add_widget = _autowrap("add_widget") - self.stop = _autowrap("stop") - self.wait = _autowrap("wait") - self.wait_until = _autowrap("wait_until") - - def _autowrap_ctx_manager(fnname): - def wrapper(*args, **kwargs): - return CtxManagerAsyncWrapped( - self.qtbot, self.qt_thread_gateway, fnname, *args, **kwargs - ) - - wrapper.__name__ = f"{fnname}" - return wrapper - - self.wait_signal = _autowrap_ctx_manager("wait_signal") - self.wait_signals = _autowrap_ctx_manager("wait_signals") - self.assert_not_emitted = _autowrap_ctx_manager("assert_not_emitted") - self.wait_active = _autowrap_ctx_manager("wait_active") - self.wait_exposed = _autowrap_ctx_manager("wait_exposed") - self.capture_exceptions = _autowrap_ctx_manager("capture_exceptions") + while True: + try: + result = callback() + except AssertionError: + result = False + if result not in (None, True, False): + msg = f"waitUntil() callback must return None, True or False, returned {result!r}" + raise ValueError(msg) + if result in (True, None): + return + if timed_out(): + raise TimeoutError(timeout_msg) + await self.wait(10) + + @asynccontextmanager + async def _wait_signals(self, signals): + if not signals: + yield + return + head, *tail = signals + async with qtrio._core.wait_signal_context(head): + async with self.wait_signals(tail): + yield + + @asynccontextmanager + async def wait_signals(self, signals, *, timeout=5000): + with trio.fail_after(timeout / 1000): + async with self._wait_signals(signals): + yield + + @asynccontextmanager + async def wait_signal(self, signal, *, timeout=5000): + async with self.wait_signals((signal,), timeout=timeout): + yield + + @asynccontextmanager + async def wait_active(self, widget, *, timeout=5000): + with self.qtbot.wait_active(widget, timeout=timeout): + yield + # TODO: make qtbot.wait_active compatible with trio + await trio.sleep(0.2) + + @asynccontextmanager + async def wait_exposed(self, widget, *, timeout=5000): + with self.qtbot.wait_exposed(widget, timeout=timeout): + yield + # TODO: make qtbot.wait_exposed compatible with trio + await trio.sleep(0.2) @pytest.fixture @@ -289,7 +197,13 @@ def _patched_submit_throttled_job(self, throttling_id, delay, *args, **kwargs): @pytest.fixture def gui_factory( - aqtbot, qtbot, qt_thread_gateway, testing_main_window_cls, core_config, event_bus_factory + aqtbot, + qtbot, + testing_main_window_cls, + job_scheduler, + core_config, + monkeypatch, + event_bus_factory, ): windows = [] @@ -316,31 +230,29 @@ async def _gui_factory( ParsecApp.connected_devices = set() - def _create_main_window(): - switch_language(core_config, "en") + # Pass minimize_on_close to avoid having test blocked by the + # closing confirmation prompt - # Pass minimize_on_close to avoid having test blocked by the - # closing confirmation prompt - main_w = testing_main_window_cls( - qt_thread_gateway._job_scheduler, event_bus, core_config, minimize_on_close=True - ) - qtbot.add_widget(main_w) - main_w.show_window(skip_dialogs=skip_dialogs) - main_w.show_top() - windows.append(main_w) - main_w.add_instance(start_arg) + switch_language(core_config, "en") - def right_main_window(): - assert ParsecApp.get_main_window() is main_w + main_w = testing_main_window_cls( + job_scheduler, event_bus, core_config, minimize_on_close=True + ) + qtbot.add_widget(main_w) + main_w.show_window(skip_dialogs=skip_dialogs) + main_w.show_top() + windows.append(main_w) + main_w.add_instance(start_arg) - # For some reasons, the main window from the previous test might - # still be around. Simply wait for things to settle down until - # our freshly created window is detected as the app main window. - qtbot.wait_until(right_main_window) + def right_main_window(): + assert ParsecApp.get_main_window() is main_w - return main_w + # For some reasons, the main window from the previous test might + # still be around. Simply wait for things to settle down until + # our freshly created window is detected as the app main window. + qtbot.wait_until(right_main_window) - return await qt_thread_gateway.send_action(_create_main_window) + return main_w return _gui_factory @@ -373,7 +285,7 @@ async def logged_gui(aqtbot, gui_factory, core_config, alice, bob, fixtures_cust @pytest.fixture -def testing_main_window_cls(aqtbot, qt_thread_gateway): +def testing_main_window_cls(aqtbot): # Since widgets are not longer persistent and are instantiated only when needed, # we can no longer simply access them. # These methods help to retrieve a widget according to the current state of the GUI. @@ -426,14 +338,9 @@ def test_get_core(self): async def test_logout(self): central_widget = self.test_get_central_widget() - - def _trigger_logout_menu(): - central_widget.button_user.menu().actions()[2].trigger() - tabw = self.test_get_tab() - async with aqtbot.wait_signal(tabw.logged_out): - await qt_thread_gateway.send_action(_trigger_logout_menu) + central_widget.button_user.menu().actions()[2].trigger() def _wait_logged_out(): assert not central_widget.isVisible() diff --git a/tests/core/gui/test_files.py b/tests/core/gui/test_files.py index 4b478115a46..4975a34f4b6 100644 --- a/tests/core/gui/test_files.py +++ b/tests/core/gui/test_files.py @@ -505,9 +505,7 @@ def _paste_failed(): @pytest.mark.gui @pytest.mark.trio -async def test_drag_and_drop( - qt_thread_gateway, tmpdir, aqtbot, autoclose_dialog, files_widget_testbed -): +async def test_drag_and_drop(tmpdir, aqtbot, autoclose_dialog, files_widget_testbed): tb = files_widget_testbed f_w = files_widget_testbed.files_widget @@ -529,7 +527,7 @@ def _import_file(): # Good drap&drop - await qt_thread_gateway.send_action(_import_file) + _import_file() await tb.check_files_view(path="/", expected_entries=["file1.txt"]) # Drap&drop in readonly workspace is not allowed @@ -537,7 +535,7 @@ def _import_file(): # Quick hack to have a read-only workspace ;-) f_w.table_files.current_user_role = WorkspaceRole.READER - await qt_thread_gateway.send_action(_import_file) + _import_file() def _import_failed(): assert autoclose_dialog.dialogs == [("Error", _("TEXT_FILE_DROP_WORKSPACE_IS_READ_ONLY"))] diff --git a/tests/core/gui/test_login.py b/tests/core/gui/test_login.py index b2492aa65f7..668474ace7b 100644 --- a/tests/core/gui/test_login.py +++ b/tests/core/gui/test_login.py @@ -136,7 +136,7 @@ async def test_login_device_list(aqtbot, gui_factory, autoclose_dialog, core_con @pytest.mark.gui @pytest.mark.trio async def test_login_no_available_devices( - aqtbot, gui_factory, autoclose_dialog, core_config, alice, qt_thread_gateway + aqtbot, gui_factory, autoclose_dialog, core_config, alice ): password = "P@ssw0rd" save_device_with_password(core_config.config_dir, alice, password) @@ -149,10 +149,7 @@ async def test_login_no_available_devices( lw = gui.test_get_login_widget() - def _reload_devices(): - lw.reload_devices() - - await qt_thread_gateway.send_action(_reload_devices) + lw.reload_devices() no_device_w = lw.widget.layout().itemAt(0).widget() assert isinstance(no_device_w, LoginNoDevicesWidget) diff --git a/tests/core/gui/test_trio_thread.py b/tests/core/gui/test_trio_thread.py deleted file mode 100644 index 9758751aa6f..00000000000 --- a/tests/core/gui/test_trio_thread.py +++ /dev/null @@ -1,51 +0,0 @@ -# Parsec Cloud (https://parsec.cloud) Copyright (c) AGPLv3 2016-2021 Scille SAS - -import trio -import threading -from unittest.mock import MagicMock - -from parsec.core.gui.trio_thread import ( - run_trio_thread, - ThreadSafeQtSignal, - JobSchedulerNotAvailable, -) - - -def test_on_trio_loop_closed(monkeypatch): - trio_loop_closed = threading.Event() - vanilla_trio_run = trio.run - - def patched_trio_run(*args, **kwargs): - try: - vanilla_trio_run(*args, **kwargs) - finally: - trio_loop_closed.set() - - monkeypatch.setattr("trio.run", patched_trio_run) - - on_success = MagicMock(spec=ThreadSafeQtSignal, args_types=()) - on_error_j1 = MagicMock(spec=ThreadSafeQtSignal, args_types=()) - on_error_j2 = MagicMock(spec=ThreadSafeQtSignal, args_types=()) - with run_trio_thread() as job_scheduler: - job1 = job_scheduler.submit_job(on_success, on_error_j1, trio.sleep_forever) - - # Stop the trio loop - job_scheduler.stop() - trio_loop_closed.wait() - - # Stop is idempotent - job_scheduler.stop() - - # Cancelling job is still ok - job1.cancel_and_join() - on_success.emit.assert_not_called() - on_error_j1.emit.assert_called_once() - assert job1.status == "cancelled" - assert isinstance(job1.exc, trio.Cancelled) - - # New jobs are directly cancelled - job2 = job_scheduler.submit_job(on_success, on_error_j2, lambda: None) - on_success.emit.assert_not_called() - on_error_j2.emit.assert_called_once() - assert job2.status == "cancelled" - assert isinstance(job2.exc, JobSchedulerNotAvailable) From 70545df9a5b581dff07d13c668b127e769513528 Mon Sep 17 00:00:00 2001 From: Vincent Michel Date: Fri, 16 Jul 2021 15:01:05 +0200 Subject: [PATCH 04/35] Fix trio job scheduler --- parsec/core/gui/trio_thread.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/parsec/core/gui/trio_thread.py b/parsec/core/gui/trio_thread.py index 9ad09de4cb8..ccf4db87eb7 100644 --- a/parsec/core/gui/trio_thread.py +++ b/parsec/core/gui/trio_thread.py @@ -182,14 +182,15 @@ async def _throttled_execute(task_status=trio.TASK_STATUS_IGNORED): self.nursery.start_soon(_throttled_execute) def submit_job(self, qt_on_success, qt_on_error, fn, *args, **kwargs): - if self.nursery._closed: - raise JobSchedulerNotAvailable job = QtToTrioJob(fn, args, kwargs, qt_on_success, qt_on_error) - self.nursery.start_soon(job) + if self.nursery._closed: + job.set_cancelled(JobSchedulerNotAvailable()) + else: + self.nursery.start_soon(job) return job def run_sync(self, fn, *args): - if self.nursery_closed: + if self.nursery._closed: raise JobSchedulerNotAvailable return fn(*args) From 5ce1c560ebd78f82a897c62356c87c7af790716e Mon Sep 17 00:00:00 2001 From: Vincent Michel Date: Fri, 16 Jul 2021 15:02:13 +0200 Subject: [PATCH 05/35] Update tests --- tests/core/gui/conftest.py | 29 ++++++++++++++++------ tests/core/gui/test_create_organization.py | 15 ++--------- 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/tests/core/gui/conftest.py b/tests/core/gui/conftest.py index c41d51c7e73..682bd5593f5 100644 --- a/tests/core/gui/conftest.py +++ b/tests/core/gui/conftest.py @@ -8,6 +8,7 @@ import qtrio import pytest from PyQt5 import QtCore +from pytestqt import exceptions as pytestqt_exceptions from parsec import __version__ as parsec_version from parsec.core.local_device import save_device_with_password @@ -108,17 +109,29 @@ async def wait_signal(self, signal, *, timeout=5000): @asynccontextmanager async def wait_active(self, widget, *, timeout=5000): - with self.qtbot.wait_active(widget, timeout=timeout): - yield - # TODO: make qtbot.wait_active compatible with trio - await trio.sleep(0.2) + yield + deadline = time.time() + timeout / 1000 + while time.time() < deadline: + try: + with self.qtbot.wait_active(widget, timeout=10): + await trio.sleep(0.01) + except pytestqt_exceptions.TimeoutError: + continue + else: + return @asynccontextmanager async def wait_exposed(self, widget, *, timeout=5000): - with self.qtbot.wait_exposed(widget, timeout=timeout): - yield - # TODO: make qtbot.wait_exposed compatible with trio - await trio.sleep(0.2) + yield + deadline = time.time() + timeout / 1000 + while time.time() < deadline: + try: + with self.qtbot.wait_exposed(widget, timeout=10): + await trio.sleep(0.01) + except pytestqt_exceptions.TimeoutError: + continue + else: + return @pytest.fixture diff --git a/tests/core/gui/test_create_organization.py b/tests/core/gui/test_create_organization.py index eae67b7eff6..72cf7f365d7 100644 --- a/tests/core/gui/test_create_organization.py +++ b/tests/core/gui/test_create_organization.py @@ -229,7 +229,6 @@ def _next_page_ready(): async def test_create_organization_bootstrap_only( aqtbot, running_backend, - qt_thread_gateway, catch_create_org_widget, autoclose_dialog, gui_factory, @@ -301,7 +300,6 @@ def _modal_shown(): async def test_create_organization_bootstrap_only_custom_server( aqtbot, running_backend, - qt_thread_gateway, catch_create_org_widget, autoclose_dialog, gui_factory, @@ -441,13 +439,7 @@ def _modal_shown(): @customize_fixtures(backend_spontaneous_organization_boostrap=True) @customize_fixtures(fake_preferred_org_creation_backend_addr=True) async def test_create_organization_custom_backend( - gui, - aqtbot, - running_backend, - catch_create_org_widget, - autoclose_dialog, - unused_tcp_port, - qt_thread_gateway, + gui, aqtbot, running_backend, catch_create_org_widget, autoclose_dialog, unused_tcp_port ): # The org creation window is usually opened using a sub-menu. # Sub-menus can be a bit challenging to open in tests so we cheat @@ -517,11 +509,8 @@ def _user_widget_ready_again(): await aqtbot.wait_until(_user_widget_ready_again) - def _select_radio_custom(): - co_w.user_widget.radio_use_custom.setChecked(True) - # Clicking the radio doesn't do anything, so we cheat - await qt_thread_gateway.send_action(_select_radio_custom) + co_w.user_widget.radio_use_custom.setChecked(True) await aqtbot.key_clicks(co_w.user_widget.line_edit_backend_addr, running_backend.addr.to_url()) await aqtbot.wait_until(_user_widget_button_validate_ready) From 9c5360efeb3d3166646a6afe9cdd5bfa46eccd26 Mon Sep 17 00:00:00 2001 From: Vincent Michel Date: Tue, 20 Jul 2021 13:18:46 +0200 Subject: [PATCH 06/35] Make sure the backend is ready before creating the GUI if necessary --- tests/core/gui/conftest.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/tests/core/gui/conftest.py b/tests/core/gui/conftest.py index 682bd5593f5..98089ff8fa9 100644 --- a/tests/core/gui/conftest.py +++ b/tests/core/gui/conftest.py @@ -211,12 +211,11 @@ def _patched_submit_throttled_job(self, throttling_id, delay, *args, **kwargs): @pytest.fixture def gui_factory( aqtbot, - qtbot, - testing_main_window_cls, job_scheduler, + testing_main_window_cls, core_config, - monkeypatch, event_bus_factory, + running_backend_ready, ): windows = [] @@ -227,6 +226,9 @@ async def _gui_factory( skip_dialogs=True, throttle_job_no_wait=True, ): + # Wait for the backend to run if necessary + await running_backend_ready.wait() + # First start popup blocks the test # Check version and mountpoint are useless for most tests core_config = core_config.evolve( @@ -251,7 +253,7 @@ async def _gui_factory( main_w = testing_main_window_cls( job_scheduler, event_bus, core_config, minimize_on_close=True ) - qtbot.add_widget(main_w) + aqtbot.qtbot.add_widget(main_w) main_w.show_window(skip_dialogs=skip_dialogs) main_w.show_top() windows.append(main_w) @@ -263,7 +265,8 @@ def right_main_window(): # For some reasons, the main window from the previous test might # still be around. Simply wait for things to settle down until # our freshly created window is detected as the app main window. - qtbot.wait_until(right_main_window) + # TODO: investigate why `await aqtbot.wait_until(right_main_window)` fails + aqtbot.qtbot.wait_until(right_main_window) return main_w From e717590fb45070b719678ab1aa4b1ca656cae237 Mon Sep 17 00:00:00 2001 From: Vincent Michel Date: Tue, 20 Jul 2021 13:20:14 +0200 Subject: [PATCH 07/35] Fix a few tests --- parsec/core/gui/trio_thread.py | 4 ++++ tests/core/gui/conftest.py | 3 +++ 2 files changed, 7 insertions(+) diff --git a/parsec/core/gui/trio_thread.py b/parsec/core/gui/trio_thread.py index ccf4db87eb7..79ba1c89dee 100644 --- a/parsec/core/gui/trio_thread.py +++ b/parsec/core/gui/trio_thread.py @@ -128,6 +128,10 @@ def _set_done(self): def cancel(self): self.cancel_scope.cancel() + def cancel_and_join(self): + # XXX: Quick fix, joining cannot be done in a callback + self.cancel_scope.cancel() + class QtToTrioJobScheduler: def __init__(self, nursery): diff --git a/tests/core/gui/conftest.py b/tests/core/gui/conftest.py index 98089ff8fa9..988585b1e65 100644 --- a/tests/core/gui/conftest.py +++ b/tests/core/gui/conftest.py @@ -438,6 +438,9 @@ async def test_switch_to_files_widget(self, workspace_name, error=False): f_w = self.test_get_files_widget() async with aqtbot.wait_exposed(f_w), aqtbot.wait_signal(f_w.folder_changed): + # We need to make sure the workspace button is ready for left click first + await aqtbot.wait_until(wk_button.switch_button.isChecked) + # Send the click await aqtbot.mouse_click(wk_button, QtCore.Qt.LeftButton) return f_w From b9d76e785af9d48faf3bc03d6f06d3b2de60e35c Mon Sep 17 00:00:00 2001 From: Vincent Michel Date: Wed, 21 Jul 2021 12:06:52 +0200 Subject: [PATCH 08/35] Force a load of current directory when switching workspaces --- parsec/core/gui/files_widget.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/parsec/core/gui/files_widget.py b/parsec/core/gui/files_widget.py index 7a2bdadfb18..108a7e4a4c5 100644 --- a/parsec/core/gui/files_widget.py +++ b/parsec/core/gui/files_widget.py @@ -167,7 +167,7 @@ async def _do_folder_stat(workspace_fs, path, default_selection, set_path): child_stat = await workspace_fs.path_info(path / child) except FSFileNotFoundError: # The child entry as been concurrently removed, just ignore it - pass + continue except FSRemoteManifestNotFound as exc: # Cannot get informations about this child entry, this can occur if # if the manifest is inconsistent (broken data or signature). @@ -366,6 +366,8 @@ def set_workspace_fs( ): self.current_directory = current_directory self.workspace_fs = wk_fs + self.load(current_directory) + ws_entry = self.jobs_ctx.run_sync(self.workspace_fs.get_workspace_entry) self.current_user_role = ws_entry.role self.label_role.setText(get_role_translation(self.current_user_role)) From 09f147e66e3126feeb7ed2afa014fc85e355e715 Mon Sep 17 00:00:00 2001 From: Vincent Michel Date: Wed, 21 Jul 2021 12:08:13 +0200 Subject: [PATCH 09/35] Prevent submit_throttled_job from failing if the nursery is closed --- parsec/core/gui/trio_thread.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/parsec/core/gui/trio_thread.py b/parsec/core/gui/trio_thread.py index 79ba1c89dee..597af283791 100644 --- a/parsec/core/gui/trio_thread.py +++ b/parsec/core/gui/trio_thread.py @@ -150,10 +150,7 @@ def submit_throttled_job( a single execution of the last provided job parameters at the soonest delay. """ - async def _throttled_execute(task_status=trio.TASK_STATUS_IGNORED): - # Create the job but don't execute it: we have to handle throttle first ! - job = QtToTrioJob(fn, args, kwargs, qt_on_success, qt_on_error) - + async def _throttled_execute(job, task_status=trio.TASK_STATUS_IGNORED): # Only modify `_throttling_scheduled_jobs` from the trio # thread to avoid concurrent acces with the Qt thread # Note we might be overwritting another job here, it is fine given @@ -183,7 +180,13 @@ async def _throttled_execute(task_status=trio.TASK_STATUS_IGNORED): # Job scheduler has been closed, nothing more can be done pass - self.nursery.start_soon(_throttled_execute) + # Create the job but don't execute it: we have to handle throttle first ! + job = QtToTrioJob(fn, args, kwargs, qt_on_success, qt_on_error) + if self.nursery._closed: + job.set_cancelled(JobSchedulerNotAvailable()) + else: + self.nursery.start_soon(_throttled_execute, job) + return job def submit_job(self, qt_on_success, qt_on_error, fn, *args, **kwargs): job = QtToTrioJob(fn, args, kwargs, qt_on_success, qt_on_error) From c58e90e65ee260b589a5353655c23f3a8eed96f9 Mon Sep 17 00:00:00 2001 From: Vincent Michel Date: Wed, 21 Jul 2021 12:09:15 +0200 Subject: [PATCH 10/35] Use qtrio runner for GUI tests --- tests/conftest.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index 1570b4574ea..61da40a205f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -196,6 +196,10 @@ def pytest_collection_modifyitems(config, items): for item in items: if "trio" in item.keywords: item.fixturenames.append("task_monitoring") + if "gui" in item.keywords and "trio" in item.keywords: + import qtrio + + item.add_marker(pytest.mark.trio(run=qtrio.run)) @pytest.fixture From 69b91590fbae300c8ed1886c961ed70e072b283c Mon Sep 17 00:00:00 2001 From: Vincent Michel Date: Wed, 21 Jul 2021 12:11:47 +0200 Subject: [PATCH 11/35] Fix a few race conditions in the GUI tests --- tests/core/gui/conftest.py | 2 ++ tests/core/gui/devices_widget/test_claim.py | 12 ++++++------ tests/core/gui/test_main_window.py | 8 ++++++++ tests/core/gui/test_offline.py | 10 ++++++---- tests/core/gui/users_widget/test_claim.py | 12 ++++++------ 5 files changed, 28 insertions(+), 16 deletions(-) diff --git a/tests/core/gui/conftest.py b/tests/core/gui/conftest.py index 988585b1e65..b94ebc0b4b5 100644 --- a/tests/core/gui/conftest.py +++ b/tests/core/gui/conftest.py @@ -443,6 +443,8 @@ async def test_switch_to_files_widget(self, workspace_name, error=False): # Send the click await aqtbot.mouse_click(wk_button, QtCore.Qt.LeftButton) + # Wait for the spinner to disappear + await aqtbot.wait_until(f_w.spinner.isHidden) return f_w async def test_switch_to_logged_in(self, device): diff --git a/tests/core/gui/devices_widget/test_claim.py b/tests/core/gui/devices_widget/test_claim.py index 03708b64389..d6ae896bf81 100644 --- a/tests/core/gui/devices_widget/test_claim.py +++ b/tests/core/gui/devices_widget/test_claim.py @@ -595,13 +595,13 @@ async def test_claim_device_offline_backend( with running_backend.offline(): await aqtbot.run(gui.add_instance, invitation_addr.to_url()) - def _assert_dialogs(): - assert len(autoclose_dialog.dialogs) == 1 - assert autoclose_dialog.dialogs == [ - ("Error", translate("TEXT_INVITATION_BACKEND_NOT_AVAILABLE")) - ] + def _assert_dialogs(): + assert len(autoclose_dialog.dialogs) == 1 + assert autoclose_dialog.dialogs == [ + ("Error", translate("TEXT_INVITATION_BACKEND_NOT_AVAILABLE")) + ] - await aqtbot.wait_until(_assert_dialogs) + await aqtbot.wait_until(_assert_dialogs) @pytest.mark.gui diff --git a/tests/core/gui/test_main_window.py b/tests/core/gui/test_main_window.py index ee127835769..f20d4c62995 100644 --- a/tests/core/gui/test_main_window.py +++ b/tests/core/gui/test_main_window.py @@ -610,6 +610,14 @@ async def test_outsider_profil_limit( await gui.test_switch_to_logged_in(adam) w_w = await gui.test_switch_to_workspaces_widget() + + def _workspace_button_shown(): + layout_workspace = w_w.layout_workspaces.itemAt(0) + assert layout_workspace is not None + workspace_button = layout_workspace.widget() + assert not isinstance(workspace_button, QtWidgets.QLabel) + + await aqtbot.wait_until(_workspace_button_shown) layout_workspace = w_w.layout_workspaces.itemAt(0) workspace_button = layout_workspace.widget() assert workspace_button.button_share.isVisible() is False diff --git a/tests/core/gui/test_offline.py b/tests/core/gui/test_offline.py index ed1aa3b7d50..ddeaef72080 100644 --- a/tests/core/gui/test_offline.py +++ b/tests/core/gui/test_offline.py @@ -7,14 +7,16 @@ @pytest.mark.gui @pytest.mark.trio async def test_offline_notification(aqtbot, running_backend, logged_gui): - central_widget = logged_gui.test_get_central_widget() assert central_widget is not None # Assert connected - assert central_widget.menu.label_connection_state.text() == translate( - "TEXT_BACKEND_STATE_CONNECTED" - ) + def _online(): + assert central_widget.menu.label_connection_state.text() == translate( + "TEXT_BACKEND_STATE_CONNECTED" + ) + + await aqtbot.wait_until(_online) # Assert offline def _offline(): diff --git a/tests/core/gui/users_widget/test_claim.py b/tests/core/gui/users_widget/test_claim.py index 7e32b595a93..9e5aa421022 100644 --- a/tests/core/gui/users_widget/test_claim.py +++ b/tests/core/gui/users_widget/test_claim.py @@ -644,13 +644,13 @@ async def test_claim_user_offline_backend( with running_backend.offline(): await aqtbot.run(gui.add_instance, invitation_addr.to_url()) - def _assert_dialogs(): - assert len(autoclose_dialog.dialogs) == 1 - assert autoclose_dialog.dialogs == [ - ("Error", translate("TEXT_INVITATION_BACKEND_NOT_AVAILABLE")) - ] + def _assert_dialogs(): + assert len(autoclose_dialog.dialogs) == 1 + assert autoclose_dialog.dialogs == [ + ("Error", translate("TEXT_INVITATION_BACKEND_NOT_AVAILABLE")) + ] - await aqtbot.wait_until(_assert_dialogs) + await aqtbot.wait_until(_assert_dialogs) @pytest.mark.gui From 287a06300f7fc767ffb4ff9cfad78b24bf24c768 Mon Sep 17 00:00:00 2001 From: Vincent Michel Date: Wed, 21 Jul 2021 12:13:04 +0200 Subject: [PATCH 12/35] Remove qt_thread_gateway from GUI tests signatures --- tests/core/gui/test_password_change.py | 40 ++++----------- tests/core/gui/test_workspace_sharing.py | 65 ++++++++++-------------- tests/core/gui/test_workspaces.py | 11 +--- 3 files changed, 38 insertions(+), 78 deletions(-) diff --git a/tests/core/gui/test_password_change.py b/tests/core/gui/test_password_change.py index 053def194a8..e1c35780bb5 100644 --- a/tests/core/gui/test_password_change.py +++ b/tests/core/gui/test_password_change.py @@ -15,21 +15,13 @@ def catch_password_change_widget(widget_catcher_factory): @pytest.mark.gui @pytest.mark.trio async def test_change_password_invalid_old_password( - aqtbot, - running_backend, - logged_gui, - catch_password_change_widget, - autoclose_dialog, - qt_thread_gateway, + aqtbot, running_backend, logged_gui, catch_password_change_widget, autoclose_dialog ): c_w = logged_gui.test_get_central_widget() - assert c_w is not None - def _trigger_password_change(): - c_w.button_user.menu().actions()[0].trigger() - - await qt_thread_gateway.send_action(_trigger_password_change) + # Trigger password change + c_w.button_user.menu().actions()[0].trigger() pc_w = await catch_password_change_widget() @@ -46,21 +38,14 @@ def _trigger_password_change(): @pytest.mark.gui @pytest.mark.trio async def test_change_password_invalid_password_check( - aqtbot, - running_backend, - logged_gui, - catch_password_change_widget, - autoclose_dialog, - qt_thread_gateway, + aqtbot, running_backend, logged_gui, catch_password_change_widget, autoclose_dialog ): c_w = logged_gui.test_get_central_widget() assert c_w is not None - def _trigger_password_change(): - c_w.button_user.menu().actions()[0].trigger() - - await qt_thread_gateway.send_action(_trigger_password_change) + # Trigger password change + c_w.button_user.menu().actions()[0].trigger() pc_w = await catch_password_change_widget() @@ -73,21 +58,14 @@ def _trigger_password_change(): @pytest.mark.gui @pytest.mark.trio async def test_change_password_success( - aqtbot, - running_backend, - logged_gui, - catch_password_change_widget, - autoclose_dialog, - qt_thread_gateway, + aqtbot, running_backend, logged_gui, catch_password_change_widget, autoclose_dialog ): c_w = logged_gui.test_get_central_widget() assert c_w is not None - def _trigger_password_change(): - c_w.button_user.menu().actions()[0].trigger() - - await qt_thread_gateway.send_action(_trigger_password_change) + # Trigger password change + c_w.button_user.menu().actions()[0].trigger() pc_w = await catch_password_change_widget() diff --git a/tests/core/gui/test_workspace_sharing.py b/tests/core/gui/test_workspace_sharing.py index d41850ea229..9775842e602 100644 --- a/tests/core/gui/test_workspace_sharing.py +++ b/tests/core/gui/test_workspace_sharing.py @@ -78,20 +78,20 @@ def _users_listed(): async def test_share_workspace( aqtbot, running_backend, + logged_gui, gui_workspace_sharing, autoclose_dialog, core_config, alice, adam, catch_share_workspace_widget, - qt_thread_gateway, monkeypatch, ): password = "P@ssw0rd" save_device_with_password(core_config.config_dir, alice, password) save_device_with_password(core_config.config_dir, adam, password) - logged_gui, w_w, share_w_w = gui_workspace_sharing + _, w_w, share_w_w = gui_workspace_sharing # Fix the return value of ensure_string_size, because it can depend of the size of the window monkeypatch.setattr( @@ -109,11 +109,8 @@ def _users_listed(): user_name = user_w.user_info.short_user_display user_w.status_timer.setInterval(200) - def _set_manager(): - user_w.combo_role.setCurrentIndex(3) - async with aqtbot.wait_signal(share_w_w.share_success): - await qt_thread_gateway.send_action(_set_manager) + user_w.combo_role.setCurrentIndex(3) async with aqtbot.wait_signal(user_w.status_timer.timeout): @@ -134,11 +131,7 @@ def _timer_stopped(): # sporadic segfault, causing the test to become inconsistent parent = share_w_w.parent().parent() async with aqtbot.wait_signals([parent.closing, w_w.list_success]): - - def _close_dialog(): - parent.reject() - - await qt_thread_gateway.send_action(_close_dialog) + parent.reject() def _workspace_listed(): assert w_w.layout_workspaces.count() == 1 @@ -215,9 +208,9 @@ def _users_listed(): @pytest.mark.gui @pytest.mark.trio async def test_share_workspace_offline( - aqtbot, running_backend, gui_workspace_sharing, autoclose_dialog, qt_thread_gateway + aqtbot, running_backend, logged_gui, gui_workspace_sharing, autoclose_dialog ): - logged_gui, w_w, share_w_w = gui_workspace_sharing + _, w_w, share_w_w = gui_workspace_sharing def _users_listed(): assert share_w_w.scroll_content.layout().count() == 4 @@ -227,17 +220,17 @@ def _users_listed(): user_w = share_w_w.scroll_content.layout().itemAt(1).widget() assert user_w.combo_role.currentIndex() == 0 - def _set_manager(): - user_w.combo_role.setCurrentIndex(3) - with running_backend.offline(): - await qt_thread_gateway.send_action(_set_manager) + user_w.combo_role.setCurrentIndex(3) - def _error_shown(): - assert len(autoclose_dialog.dialogs) == 1 - assert autoclose_dialog.dialogs[0] == ("Error", translate("TEXT_WORKSPACE_SHARING_OFFLINE")) + def _error_shown(): + assert len(autoclose_dialog.dialogs) == 1 + assert autoclose_dialog.dialogs[0] == ( + "Error", + translate("TEXT_WORKSPACE_SHARING_OFFLINE"), + ) - await aqtbot.wait_until(_error_shown) + await aqtbot.wait_until(_error_shown) @pytest.mark.gui @@ -247,9 +240,9 @@ def _error_shown(): @customize_fixtures(logged_gui_as_admin=True) @customize_fixtures(bob_profile=UserProfile.OUTSIDER) async def test_share_with_outsider_limit_roles( - aqtbot, running_backend, gui_workspace_sharing, autoclose_dialog, qt_thread_gateway + aqtbot, running_backend, logged_gui, gui_workspace_sharing, autoclose_dialog ): - logged_gui, w_w, share_w_w = gui_workspace_sharing + _, w_w, share_w_w = gui_workspace_sharing def _users_listed(): assert share_w_w.scroll_content.layout().count() == 4 @@ -258,14 +251,11 @@ def _users_listed(): for role_index, role_name in [(3, "Manager"), (4, "Owner")]: - def _set_manager(): - select_bob_w = share_w_w.scroll_content.layout().itemAt(2).widget() - assert select_bob_w.label_email.text() == "bob@example.com" - # Switch bob to an invalid role - assert select_bob_w.combo_role.itemText(role_index) == role_name - select_bob_w.combo_role.setCurrentIndex(3) - - await qt_thread_gateway.send_action(_set_manager) + select_bob_w = share_w_w.scroll_content.layout().itemAt(2).widget() + assert select_bob_w.label_email.text() == "bob@example.com" + # Switch bob to an invalid role + assert select_bob_w.combo_role.itemText(role_index) == role_name + select_bob_w.combo_role.setCurrentIndex(3) def _error_shown(): assert len(autoclose_dialog.dialogs) == 1 @@ -282,7 +272,7 @@ def _error_shown(): @pytest.mark.gui @pytest.mark.trio async def test_workspace_sharing_filter_users( - aqtbot, running_backend, gui_workspace_sharing, autoclose_dialog, qt_thread_gateway + aqtbot, running_backend, gui_workspace_sharing, autoclose_dialog ): logged_gui, w_w, share_w_w = gui_workspace_sharing @@ -306,24 +296,25 @@ def _reset_input(): await aqtbot.key_clicks(share_w_w.line_edit_filter, "face") assert _users_visible() == 3 - await qt_thread_gateway.send_action(_reset_input) + _reset_input() await aqtbot.key_clicks(share_w_w.line_edit_filter, "mca") assert _users_visible() == 2 - await qt_thread_gateway.send_action(_reset_input) + _reset_input() await aqtbot.key_clicks(share_w_w.line_edit_filter, "bob") assert _users_visible() == 1 - await qt_thread_gateway.send_action(_reset_input) + _reset_input() await aqtbot.key_clicks(share_w_w.line_edit_filter, "zoidberg") assert _users_visible() == 0 + _reset_input() @pytest.mark.gui @pytest.mark.trio async def test_share_workspace_while_connected( - aqtbot, running_backend, logged_gui, autoclose_dialog, qt_thread_gateway, alice_user_fs, bob + aqtbot, running_backend, logged_gui, autoclose_dialog, alice_user_fs, bob ): w_w = await logged_gui.test_switch_to_workspaces_widget() wid = await alice_user_fs.workspace_create("Workspace") @@ -349,7 +340,7 @@ def _one_workspace_listed(): @pytest.mark.gui @pytest.mark.trio async def test_unshare_workspace_while_connected( - aqtbot, running_backend, logged_gui, autoclose_dialog, qt_thread_gateway, alice_user_fs, bob + aqtbot, running_backend, logged_gui, autoclose_dialog, alice_user_fs, bob ): w_w = await logged_gui.test_switch_to_workspaces_widget() wid = await alice_user_fs.workspace_create("Workspace") diff --git a/tests/core/gui/test_workspaces.py b/tests/core/gui/test_workspaces.py index b609fa783c2..40d3dad2721 100644 --- a/tests/core/gui/test_workspaces.py +++ b/tests/core/gui/test_workspaces.py @@ -262,15 +262,7 @@ def _wk_opened(): @pytest.mark.gui @pytest.mark.trio async def test_workspace_filter_user( - aqtbot, - running_backend, - logged_gui, - autoclose_dialog, - qt_thread_gateway, - alice_user_fs, - bob, - bob_user_fs, - alice, + aqtbot, running_backend, logged_gui, autoclose_dialog, alice_user_fs, bob, bob_user_fs, alice ): w_w = await logged_gui.test_switch_to_workspaces_widget() wid_alice = await alice_user_fs.workspace_create("Workspace1") @@ -339,7 +331,6 @@ async def test_workspace_filter_user_new_workspace( running_backend, logged_gui, autoclose_dialog, - qt_thread_gateway, alice_user_fs, bob, bob_user_fs, From e303e562d79e54ed94b6cc57d4c581a1b7a5860e Mon Sep 17 00:00:00 2001 From: Vincent Michel Date: Wed, 21 Jul 2021 12:19:26 +0200 Subject: [PATCH 13/35] Add newsfragment --- newsfragments/1711.misc.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 newsfragments/1711.misc.rst diff --git a/newsfragments/1711.misc.rst b/newsfragments/1711.misc.rst new file mode 100644 index 00000000000..3e33f73bd4e --- /dev/null +++ b/newsfragments/1711.misc.rst @@ -0,0 +1 @@ +Migrate to QTrio (using the trio guest loop mode) in order to have the core and the GUI running in the same thread. From 06cec244a5c5bc4d10189e542fc414e2afe126cc Mon Sep 17 00:00:00 2001 From: Vincent Michel Date: Thu, 22 Jul 2021 14:57:42 +0200 Subject: [PATCH 14/35] QApplication should not be used as it prevents the trio loop from terminating properly --- parsec/core/gui/app.py | 2 +- parsec/core/gui/main_window.py | 6 ++++-- parsec/core/gui/trio_thread.py | 3 +++ tests/core/gui/conftest.py | 2 +- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/parsec/core/gui/app.py b/parsec/core/gui/app.py index 1e9dc5d0121..671dc96e36d 100644 --- a/parsec/core/gui/app.py +++ b/parsec/core/gui/app.py @@ -155,6 +155,7 @@ async def _run_gui(config: CoreConfig, start_arg: str = None, diagnose: bool = F async with run_trio_job_scheduler() as jobs_ctx: win = MainWindow( jobs_ctx=jobs_ctx, + quit_callback=jobs_ctx.close, event_bus=event_bus, config=config, minimize_on_close=config.gui_tray_enabled and systray_available(), @@ -199,7 +200,6 @@ async def _run_gui(config: CoreConfig, start_arg: str = None, diagnose: bool = F def kill_window(*args): win.close_app(force=True) - QApplication.quit() signal.signal(signal.SIGINT, kill_window) diff --git a/parsec/core/gui/main_window.py b/parsec/core/gui/main_window.py index ecc6a946de3..0e9ec2b95cb 100644 --- a/parsec/core/gui/main_window.py +++ b/parsec/core/gui/main_window.py @@ -9,7 +9,7 @@ from PyQt5.QtCore import QCoreApplication, pyqtSignal, Qt, QSize from PyQt5.QtGui import QColor, QIcon, QKeySequence, QResizeEvent, QCloseEvent -from PyQt5.QtWidgets import QMainWindow, QApplication, QMenu, QShortcut +from PyQt5.QtWidgets import QMainWindow, QMenu, QShortcut from parsec import __version__ as PARSEC_VERSION from parsec.event_bus import EventBus, EventCallback @@ -68,6 +68,7 @@ class MainWindow(QMainWindow, Ui_MainWindow): # type: ignore[misc] def __init__( self, jobs_ctx: QtToTrioJobScheduler, + quit_callback: Callable[[], None], event_bus: EventBus, config: CoreConfig, minimize_on_close: bool = False, @@ -78,6 +79,7 @@ def __init__( self.setMenuBar(None) self.jobs_ctx = jobs_ctx + self.quit_callback = quit_callback self.event_bus = event_bus self.config = config self.minimize_on_close = minimize_on_close @@ -744,4 +746,4 @@ def closeEvent(self, event: QCloseEvent) -> None: self.event_bus.send(CoreEvent.GUI_CONFIG_CHANGED, gui_geometry=state) self.close_all_tabs() event.accept() - QApplication.quit() + self.quit_callback() diff --git a/parsec/core/gui/trio_thread.py b/parsec/core/gui/trio_thread.py index 597af283791..ffecd3b5387 100644 --- a/parsec/core/gui/trio_thread.py +++ b/parsec/core/gui/trio_thread.py @@ -139,6 +139,9 @@ def __init__(self, nursery): self._throttling_scheduled_jobs = {} self._throttling_last_executed = {} + def close(self): + self.nursery.cancel_scope.cancel() + def submit_throttled_job( self, throttling_id: str, delay: float, qt_on_success, qt_on_error, fn, *args, **kwargs ): diff --git a/tests/core/gui/conftest.py b/tests/core/gui/conftest.py index b94ebc0b4b5..bfa13fe9993 100644 --- a/tests/core/gui/conftest.py +++ b/tests/core/gui/conftest.py @@ -251,7 +251,7 @@ async def _gui_factory( switch_language(core_config, "en") main_w = testing_main_window_cls( - job_scheduler, event_bus, core_config, minimize_on_close=True + job_scheduler, job_scheduler.close, event_bus, core_config, minimize_on_close=True ) aqtbot.qtbot.add_widget(main_w) main_w.show_window(skip_dialogs=skip_dialogs) From 89a2391337d6885d0c33303e0ccd53b6cccf4811 Mon Sep 17 00:00:00 2001 From: Vincent Michel Date: Thu, 22 Jul 2021 15:08:56 +0200 Subject: [PATCH 15/35] Fix input dialog tests --- parsec/core/gui/custom_dialogs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/parsec/core/gui/custom_dialogs.py b/parsec/core/gui/custom_dialogs.py index dda9eded3e5..b67b2597452 100644 --- a/parsec/core/gui/custom_dialogs.py +++ b/parsec/core/gui/custom_dialogs.py @@ -76,7 +76,7 @@ def __init__( else: self.showMaximized() self.move(0, 0) - else: + elif parent is not None: logger.error("GreyedDialog did not find the main window, this is probably a bug") self.setFocus() self.accepted.connect(self.on_finished) From 107b699a14f26ad965a33abe2526ba6491fcbfce Mon Sep 17 00:00:00 2001 From: Vincent Michel Date: Thu, 22 Jul 2021 15:28:49 +0200 Subject: [PATCH 16/35] Remove QtToTrioJob.cancel_and_join --- parsec/core/gui/claim_device_widget.py | 16 ++++++++-------- parsec/core/gui/claim_user_widget.py | 16 ++++++++-------- parsec/core/gui/create_org_widget.py | 2 +- parsec/core/gui/file_history_widget.py | 2 +- parsec/core/gui/files_widget.py | 2 +- parsec/core/gui/greet_device_widget.py | 2 +- parsec/core/gui/greet_user_widget.py | 2 +- parsec/core/gui/new_version.py | 2 +- parsec/core/gui/timestamped_workspace_widget.py | 2 +- parsec/core/gui/trio_thread.py | 4 ---- 10 files changed, 23 insertions(+), 27 deletions(-) diff --git a/parsec/core/gui/claim_device_widget.py b/parsec/core/gui/claim_device_widget.py index b7e428074e2..d0e1a6b860e 100644 --- a/parsec/core/gui/claim_device_widget.py +++ b/parsec/core/gui/claim_device_widget.py @@ -339,13 +339,13 @@ def _on_wait_peer_trust_error(self, job): def cancel(self): if self.signify_trust_job: - self.signify_trust_job.cancel_and_join() + self.signify_trust_job.cancel() if self.wait_peer_trust_job: - self.wait_peer_trust_job.cancel_and_join() + self.wait_peer_trust_job.cancel() if self.get_claimer_sas_job: - self.get_claimer_sas_job.cancel_and_join() + self.get_claimer_sas_job.cancel() if self.get_greeter_sas_job: - self.get_greeter_sas_job.cancel_and_join() + self.get_greeter_sas_job.cancel() class ClaimDeviceProvideInfoWidget(QWidget, Ui_ClaimDeviceProvideInfoWidget): @@ -428,7 +428,7 @@ def _on_claim_error(self, job): def cancel(self): if self.claim_job: - self.claim_job.cancel_and_join() + self.claim_job.cancel() class ClaimDeviceInstructionsWidget(QWidget, Ui_ClaimDeviceInstructionsWidget): @@ -487,7 +487,7 @@ def _on_wait_peer_error(self, job): def cancel(self): if self.wait_peer_job: - self.wait_peer_job.cancel_and_join() + self.wait_peer_job.cancel() class ClaimDeviceWidget(QWidget, Ui_ClaimDeviceWidget): @@ -654,9 +654,9 @@ def cancel(self): if current_page and getattr(current_page, "cancel", None): current_page.cancel() if self.retrieve_info_job: - self.retrieve_info_job.cancel_and_join() + self.retrieve_info_job.cancel() if self.claimer_job: - self.claimer_job.cancel_and_join() + self.claimer_job.cancel() def on_close(self): self.cancel() diff --git a/parsec/core/gui/claim_user_widget.py b/parsec/core/gui/claim_user_widget.py index c5d38f1e5b4..5ff2af5cf4b 100644 --- a/parsec/core/gui/claim_user_widget.py +++ b/parsec/core/gui/claim_user_widget.py @@ -379,13 +379,13 @@ def _on_wait_peer_trust_error(self, job): def cancel(self): if self.signify_trust_job: - self.signify_trust_job.cancel_and_join() + self.signify_trust_job.cancel() if self.wait_peer_trust_job: - self.wait_peer_trust_job.cancel_and_join() + self.wait_peer_trust_job.cancel() if self.get_claimer_sas_job: - self.get_claimer_sas_job.cancel_and_join() + self.get_claimer_sas_job.cancel() if self.get_greeter_sas_job: - self.get_greeter_sas_job.cancel_and_join() + self.get_greeter_sas_job.cancel() class ClaimUserProvideInfoWidget(QWidget, Ui_ClaimUserProvideInfoWidget): @@ -484,7 +484,7 @@ def _on_claim_error(self, job): def cancel(self): if self.claim_job: - self.claim_job.cancel_and_join() + self.claim_job.cancel() class ClaimUserInstructionsWidget(QWidget, Ui_ClaimUserInstructionsWidget): @@ -543,7 +543,7 @@ def _on_wait_peer_error(self, job): def cancel(self): if self.wait_peer_job: - self.wait_peer_job.cancel_and_join() + self.wait_peer_job.cancel() class ClaimUserWidget(QWidget, Ui_ClaimUserWidget): @@ -732,9 +732,9 @@ def cancel(self): if current_page and getattr(current_page, "cancel", None): current_page.cancel() if self.retrieve_info_job: - self.retrieve_info_job.cancel_and_join() + self.retrieve_info_job.cancel() if self.claimer_job: - self.claimer_job.cancel_and_join() + self.claimer_job.cancel() def on_close(self): self.cancel() diff --git a/parsec/core/gui/create_org_widget.py b/parsec/core/gui/create_org_widget.py index 4b863527512..f87b7856323 100644 --- a/parsec/core/gui/create_org_widget.py +++ b/parsec/core/gui/create_org_widget.py @@ -212,7 +212,7 @@ def _on_info_invalid(self): def on_close(self): self.status = None if self.create_job: - self.create_job.cancel_and_join() + self.create_job.cancel() def _on_previous_clicked(self): self.user_widget.show() diff --git a/parsec/core/gui/file_history_widget.py b/parsec/core/gui/file_history_widget.py index c3933dab6d4..89bdc10b55e 100644 --- a/parsec/core/gui/file_history_widget.py +++ b/parsec/core/gui/file_history_widget.py @@ -146,7 +146,7 @@ def on_get_version_error(self): def on_close(self): if self.versions_job: - self.versions_job.cancel_and_join() + self.versions_job.cancel() @classmethod def show_modal( diff --git a/parsec/core/gui/files_widget.py b/parsec/core/gui/files_widget.py index 108a7e4a4c5..4c323c81e98 100644 --- a/parsec/core/gui/files_widget.py +++ b/parsec/core/gui/files_widget.py @@ -685,7 +685,7 @@ def cancel_import(self): assert self.import_job assert self.loading_dialog - self.import_job.cancel_and_join() + self.import_job.cancel() def _on_import_progress(self, file_name, progress): if not self.loading_dialog: diff --git a/parsec/core/gui/greet_device_widget.py b/parsec/core/gui/greet_device_widget.py index edc38829268..c74e0bae03b 100644 --- a/parsec/core/gui/greet_device_widget.py +++ b/parsec/core/gui/greet_device_widget.py @@ -520,7 +520,7 @@ def cancel(self): if current_page and getattr(current_page, "cancel", None): current_page.cancel() if self.greeter_job: - self.greeter_job.cancel_and_join() + self.greeter_job.cancel() def on_close(self): self.cancel() diff --git a/parsec/core/gui/greet_user_widget.py b/parsec/core/gui/greet_user_widget.py index 4754b69013f..61a52d079e2 100644 --- a/parsec/core/gui/greet_user_widget.py +++ b/parsec/core/gui/greet_user_widget.py @@ -660,7 +660,7 @@ def cancel(self): if current_page and getattr(current_page, "cancel", None): current_page.cancel() if self.greeter_job: - self.greeter_job.cancel_and_join() + self.greeter_job.cancel() def on_close(self): self.cancel() diff --git a/parsec/core/gui/new_version.py b/parsec/core/gui/new_version.py index d77480cfd68..25c43c58f41 100644 --- a/parsec/core/gui/new_version.py +++ b/parsec/core/gui/new_version.py @@ -250,5 +250,5 @@ def ignore(self): def closeEvent(self, event): if self.version_job: - self.version_job.cancel_and_join() + self.version_job.cancel() event.accept() diff --git a/parsec/core/gui/timestamped_workspace_widget.py b/parsec/core/gui/timestamped_workspace_widget.py index ef024886287..de37f86abe9 100644 --- a/parsec/core/gui/timestamped_workspace_widget.py +++ b/parsec/core/gui/timestamped_workspace_widget.py @@ -64,7 +64,7 @@ def _on_show_clicked(self): def cancel(self): if self.limits_job: - self.limits_job.cancel_and_join() + self.limits_job.cancel() def set_time_limits(self): selected_date = self.calendar_widget.selectedDate() diff --git a/parsec/core/gui/trio_thread.py b/parsec/core/gui/trio_thread.py index ffecd3b5387..53386a0b520 100644 --- a/parsec/core/gui/trio_thread.py +++ b/parsec/core/gui/trio_thread.py @@ -128,10 +128,6 @@ def _set_done(self): def cancel(self): self.cancel_scope.cancel() - def cancel_and_join(self): - # XXX: Quick fix, joining cannot be done in a callback - self.cancel_scope.cancel() - class QtToTrioJobScheduler: def __init__(self, nursery): From bd095263e3cda61956f53ef7667cd36c179470a5 Mon Sep 17 00:00:00 2001 From: Vincent Michel Date: Thu, 22 Jul 2021 15:47:01 +0200 Subject: [PATCH 17/35] Remove aqtbot.run --- tests/core/gui/conftest.py | 5 +--- tests/core/gui/devices_widget/test_claim.py | 24 +++++++++--------- tests/core/gui/devices_widget/test_greet.py | 8 +++--- tests/core/gui/devices_widget/test_invite.py | 2 +- tests/core/gui/test_create_organization.py | 2 +- tests/core/gui/test_file_history.py | 6 ++--- tests/core/gui/test_files.py | 6 ++--- tests/core/gui/test_main_window.py | 26 ++++++++++---------- tests/core/gui/users_widget/test_claim.py | 24 +++++++++--------- tests/core/gui/users_widget/test_greet.py | 8 +++--- tests/core/gui/users_widget/test_invite.py | 6 ++--- 11 files changed, 56 insertions(+), 61 deletions(-) diff --git a/tests/core/gui/conftest.py b/tests/core/gui/conftest.py index bfa13fe9993..eb791f8aab3 100644 --- a/tests/core/gui/conftest.py +++ b/tests/core/gui/conftest.py @@ -39,9 +39,6 @@ class AsyncQtBot: def __init__(self, qtbot): self.qtbot = qtbot - async def run(self, fn, *args, **kwargs): - return fn(*args, **kwargs) - def __getattr__(self, name): words = name.split("_") camel_name = words[0] + "".join(word.title() for word in words[1:]) @@ -455,7 +452,7 @@ async def test_switch_to_logged_in(self, device): lw = self.test_get_login_widget() # Reload to take into account the new saved device - await aqtbot.run(lw.reload_devices) + lw.reload_devices() tabw = self.test_get_tab() accounts_w = lw.widget.layout().itemAt(0).widget() diff --git a/tests/core/gui/devices_widget/test_claim.py b/tests/core/gui/devices_widget/test_claim.py index d6ae896bf81..241f1925f4c 100644 --- a/tests/core/gui/devices_widget/test_claim.py +++ b/tests/core/gui/devices_widget/test_claim.py @@ -88,7 +88,7 @@ async def bootstrap(self): # Switch to device claim page - await aqtbot.run(gui.add_instance, invitation_addr.to_url()) + gui.add_instance(invitation_addr.to_url()) cd_w = await catch_claim_device_widget() assert isinstance(cd_w, ClaimDeviceWidget) @@ -183,7 +183,7 @@ async def step_3_exchange_greeter_sas(self): cdce_w = self.claim_device_code_exchange_widget # Pretend we have choosen the right code - await aqtbot.run(cdce_w.code_input_widget.good_code_clicked.emit) + cdce_w.code_input_widget.good_code_clicked.emit() self.greeter_in_progress_ctx = await self.greeter_in_progress_ctx.do_wait_peer_trust() claimer_sas = self.greeter_in_progress_ctx.claimer_sas @@ -222,7 +222,7 @@ async def step_5_provide_claim_info(self): cdpi_w = self.claim_device_provide_info_widget device_label = self.requested_device_label - await aqtbot.run(cdpi_w.line_edit_device.clear) + cdpi_w.line_edit_device.clear() assert not cdpi_w.button_ok.isEnabled() @@ -324,7 +324,7 @@ async def offline_step_3_exchange_greeter_sas(self): with running_backend.offline(): assert not autoclose_dialog.dialogs - await aqtbot.run(cdce_w.code_input_widget.good_code_clicked.emit) + cdce_w.code_input_widget.good_code_clicked.emit() await aqtbot.wait_until(partial(self._claim_aborted, expected_message)) return None @@ -341,7 +341,7 @@ async def offline_step_5_provide_claim_info(self): device_label = self.requested_device_label with running_backend.offline(): - await aqtbot.run(cdpi_w.line_edit_device.clear) + cdpi_w.line_edit_device.clear() await aqtbot.key_clicks(cdpi_w.line_edit_device, device_label) await aqtbot.key_clicks(cdpi_w.widget_password.line_edit_password, self.password) await aqtbot.key_clicks( @@ -400,7 +400,7 @@ async def reset_step_3_exchange_greeter_sas(self): expected_message = translate("TEXT_CLAIM_DEVICE_PEER_RESET") async with self._reset_greeter(): - await aqtbot.run(cdce_w.code_input_widget.good_code_clicked.emit) + cdce_w.code_input_widget.good_code_clicked.emit() await aqtbot.wait_until(partial(self._claim_restart, expected_message)) await self.bootstrap_after_restart() @@ -420,7 +420,7 @@ async def reset_step_5_provide_claim_info(self): device_label = self.requested_device_label async with self._reset_greeter(): - await aqtbot.run(cdpi_w.line_edit_device.clear) + cdpi_w.line_edit_device.clear() await aqtbot.key_clicks(cdpi_w.line_edit_device, device_label) await aqtbot.key_clicks(cdpi_w.widget_password.line_edit_password, self.password) await aqtbot.key_clicks( @@ -501,7 +501,7 @@ async def cancelled_step_3_exchange_greeter_sas(self): cdce_w = self.claim_device_code_exchange_widget await self._cancel_invitation() - await aqtbot.run(cdce_w.code_input_widget.good_code_clicked.emit) + cdce_w.code_input_widget.good_code_clicked.emit() await aqtbot.wait_until(partial(self._claim_restart, expected_message)) return None @@ -521,7 +521,7 @@ async def cancelled_step_5_provide_claim_info(self): await self._cancel_invitation() - await aqtbot.run(cdpi_w.line_edit_device.clear) + cdpi_w.line_edit_device.clear() await aqtbot.key_clicks(cdpi_w.line_edit_device, device_label) await aqtbot.key_clicks(cdpi_w.widget_password.line_edit_password, self.password) await aqtbot.key_clicks(cdpi_w.widget_password.line_edit_password_check, self.password) @@ -568,7 +568,7 @@ async def test_claim_device_already_deleted( reason=InvitationDeletedReason.CANCELLED, ) - await aqtbot.run(gui.add_instance, invitation_addr.to_url()) + gui.add_instance(invitation_addr.to_url()) def _assert_dialogs(): assert len(autoclose_dialog.dialogs) == 1 @@ -593,7 +593,7 @@ async def test_claim_device_offline_backend( token=invitation.token, ) with running_backend.offline(): - await aqtbot.run(gui.add_instance, invitation_addr.to_url()) + gui.add_instance(invitation_addr.to_url()) def _assert_dialogs(): assert len(autoclose_dialog.dialogs) == 1 @@ -617,7 +617,7 @@ async def test_claim_device_unknown_invitation( token=uuid4(), ) - await aqtbot.run(gui.add_instance, invitation_addr.to_url()) + gui.add_instance(invitation_addr.to_url()) def _assert_dialogs(): assert len(autoclose_dialog.dialogs) == 1 diff --git a/tests/core/gui/devices_widget/test_greet.py b/tests/core/gui/devices_widget/test_greet.py index 432f86709fb..d3c9cdbc08e 100644 --- a/tests/core/gui/devices_widget/test_greet.py +++ b/tests/core/gui/devices_widget/test_greet.py @@ -199,7 +199,7 @@ async def step_4_exchange_claimer_sas(self): gdce_w = self.greet_device_code_exchange_widget # Pretent we have clicked on the right choice - await aqtbot.run(gdce_w.code_input_widget.good_code_clicked.emit) + gdce_w.code_input_widget.good_code_clicked.emit() self.claimer_in_progress_ctx = await self.claimer_in_progress_ctx.do_wait_peer_trust() @@ -300,7 +300,7 @@ async def offline_step_4_exchange_claimer_sas(self): guce_w = self.greet_device_code_exchange_widget with running_backend.offline(): - await aqtbot.run(guce_w.code_input_widget.good_code_clicked.emit) + guce_w.code_input_widget.good_code_clicked.emit() await aqtbot.wait_until(partial(self._greet_aborted, expected_message)) return None @@ -354,7 +354,7 @@ async def reset_step_4_exchange_claimer_sas(self): guce_w = self.greet_device_code_exchange_widget # Pretent we have click on the right choice - await aqtbot.run(guce_w.code_input_widget.good_code_clicked.emit) + guce_w.code_input_widget.good_code_clicked.emit() async with self._reset_claimer(): await aqtbot.wait_until(partial(self._greet_restart, expected_message)) @@ -439,7 +439,7 @@ async def cancelled_step_4_exchange_claimer_sas(self): await self._cancel_invitation() - await aqtbot.run(guce_w.code_input_widget.good_code_clicked.emit) + guce_w.code_input_widget.good_code_clicked.emit() await aqtbot.wait_until(partial(self._greet_restart, expected_message)) return None diff --git a/tests/core/gui/devices_widget/test_invite.py b/tests/core/gui/devices_widget/test_invite.py index 6180d9d1656..60dd830bf5f 100644 --- a/tests/core/gui/devices_widget/test_invite.py +++ b/tests/core/gui/devices_widget/test_invite.py @@ -242,7 +242,7 @@ def _claimer_code_choices_displayed(): # Pretend we have choosen the right code # TODO: click on button instead of sending the corresponding event - await aqtbot.run(gdce_w.code_input_widget.good_code_clicked.emit) + gdce_w.code_input_widget.good_code_clicked.emit() def _wait_claimer_info(): assert gdce_w.label_wait_info.isVisible() diff --git a/tests/core/gui/test_create_organization.py b/tests/core/gui/test_create_organization.py index 72cf7f365d7..054ae048aee 100644 --- a/tests/core/gui/test_create_organization.py +++ b/tests/core/gui/test_create_organization.py @@ -585,7 +585,7 @@ async def test_create_organization_with_boostrap_token( for bootstrap_addr in (bad_bootstrap_addr, good_bootstrap_addr): autoclose_dialog.reset() - await aqtbot.run(gui.add_instance, bootstrap_addr.to_url()) + gui.add_instance(bootstrap_addr.to_url()) co_w = await catch_create_org_widget() diff --git a/tests/core/gui/test_file_history.py b/tests/core/gui/test_file_history.py index b8bd6e19cda..f1c53ce260b 100644 --- a/tests/core/gui/test_file_history.py +++ b/tests/core/gui/test_file_history.py @@ -39,9 +39,7 @@ def _entry_available(): await aqtbot.wait_until(_entry_available) # First select the entry... - await aqtbot.run( - f_w.table_files.setRangeSelected, QtWidgets.QTableWidgetSelectionRange(1, 0, 1, 0), True - ) + f_w.table_files.setRangeSelected(QtWidgets.QTableWidgetSelectionRange(1, 0, 1, 0), True) def _entry_selected(): assert f_w.table_files.selectedItems() @@ -49,7 +47,7 @@ def _entry_selected(): await aqtbot.wait_until(_entry_selected) # ...then ask for history - await aqtbot.run(f_w.table_files.show_history_clicked.emit) + f_w.table_files.show_history_clicked.emit() hf_w = await catch_file_history_widget() diff --git a/tests/core/gui/test_files.py b/tests/core/gui/test_files.py index 4975a34f4b6..0ec967a3f39 100644 --- a/tests/core/gui/test_files.py +++ b/tests/core/gui/test_files.py @@ -104,10 +104,10 @@ def _do_selection(): QtWidgets.QTableWidgetSelectionRange(top, 0, bottom, 0), True ) - await aqtbot.run(_do_selection) + _do_selection() async def reset_selection(self): - await aqtbot.run(f_w.table_files.reset) + f_w.table_files.reset() async def copy(self, selection=None): if selection is not None: @@ -693,7 +693,7 @@ async def test_use_file_link(aqtbot, autoclose_dialog, files_widget_testbed): # Create and use file link url = f_w.workspace_fs.generate_file_link("/foo/bar.txt") - await aqtbot.run(tb.logged_gui.add_instance, str(url)) + tb.logged_gui.add_instance(str(url)) def _selection_on_file(): assert tb.pwd() == "/foo" diff --git a/tests/core/gui/test_main_window.py b/tests/core/gui/test_main_window.py index f20d4c62995..209c678d33e 100644 --- a/tests/core/gui/test_main_window.py +++ b/tests/core/gui/test_main_window.py @@ -154,7 +154,7 @@ async def test_link_file(aqtbot, logged_gui_with_files): logged_gui, w_w, f_w = logged_gui_with_files url = f_w.workspace_fs.generate_file_link(f_w.current_directory) - await aqtbot.run(logged_gui.add_instance, str(url)) + logged_gui.add_instance(str(url)) def _folder_ready(): assert f_w.isVisible() @@ -176,7 +176,7 @@ async def test_link_file_unmounted(aqtbot, logged_gui_with_files): core = logged_gui.test_get_core() url = f_w.workspace_fs.generate_file_link(f_w.current_directory) - await aqtbot.run(logged_gui.add_instance, str(url)) + logged_gui.add_instance(str(url)) def _folder_ready(): assert f_w.isVisible() @@ -200,7 +200,7 @@ def _unmounted(): await aqtbot.wait_until(_unmounted) - await aqtbot.run(logged_gui.add_instance, str(url)) + logged_gui.add_instance(str(url)) await aqtbot.wait_until(_mounted) @@ -211,7 +211,7 @@ async def test_link_file_invalid_path(aqtbot, autoclose_dialog, logged_gui_with_ logged_gui, w_w, f_w = logged_gui_with_files url = f_w.workspace_fs.generate_file_link("/unknown") - await aqtbot.run(logged_gui.add_instance, str(url)) + logged_gui.add_instance(str(url)) def _assert_dialogs(): assert len(autoclose_dialog.dialogs) == 1 @@ -235,7 +235,7 @@ async def test_link_file_invalid_url(aqtbot, autoclose_dialog, logged_gui_with_f else: assert False - await aqtbot.run(logged_gui.add_instance, url) + logged_gui.add_instance(url) def _assert_dialogs(): assert len(autoclose_dialog.dialogs) == 1 @@ -259,7 +259,7 @@ async def test_link_file_disconnected( # Log out and send link await gui.test_logout_and_switch_to_login_widget() - await aqtbot.run(gui.add_instance, addr.to_url()) + gui.add_instance(addr.to_url()) def _assert_dialogs(): assert len(autoclose_dialog.dialogs) == 1 @@ -323,7 +323,7 @@ async def test_link_file_disconnected_cancel_login( # Log out and send link await gui.test_logout_and_switch_to_login_widget() - await aqtbot.run(gui.add_instance, str(url)) + gui.add_instance(str(url)) def _assert_dialogs(): assert len(autoclose_dialog.dialogs) == 1 @@ -370,7 +370,7 @@ def _folder_ready(): async def test_link_organization( aqtbot, logged_gui, catch_create_org_widget, organization_bootstrap_addr ): - await aqtbot.run(logged_gui.add_instance, organization_bootstrap_addr.to_url()) + logged_gui.add_instance(organization_bootstrap_addr.to_url()) co_w = await catch_create_org_widget() assert co_w assert logged_gui.tab_center.count() == 2 @@ -382,7 +382,7 @@ async def test_link_organization_disconnected( aqtbot, logged_gui, catch_create_org_widget, organization_bootstrap_addr ): await logged_gui.test_logout_and_switch_to_login_widget() - await aqtbot.run(logged_gui.add_instance, organization_bootstrap_addr.to_url()) + logged_gui.add_instance(organization_bootstrap_addr.to_url()) co_w = await catch_create_org_widget() assert co_w assert logged_gui.tab_center.count() == 1 @@ -399,7 +399,7 @@ async def test_link_claim_device( else: url = device_invitation_addr.to_url() - await aqtbot.run(logged_gui.add_instance, url) + logged_gui.add_instance(url) cd_w = await catch_claim_device_widget() assert cd_w assert logged_gui.tab_center.count() == 2 @@ -411,7 +411,7 @@ async def test_link_claim_device_disconnected( aqtbot, logged_gui, catch_claim_device_widget, device_invitation_addr ): await logged_gui.test_logout_and_switch_to_login_widget() - await aqtbot.run(logged_gui.add_instance, device_invitation_addr.to_url()) + logged_gui.add_instance(device_invitation_addr.to_url()) cd_w = await catch_claim_device_widget() assert cd_w assert logged_gui.tab_center.count() == 1 @@ -428,7 +428,7 @@ async def test_link_claim_user( else: url = user_invitation_addr.to_url() - await aqtbot.run(logged_gui.add_instance, url) + logged_gui.add_instance(url) cd_w = await catch_claim_user_widget() assert cd_w assert logged_gui.tab_center.count() == 2 @@ -440,7 +440,7 @@ async def test_link_claim_user_disconnected( aqtbot, logged_gui, catch_claim_user_widget, user_invitation_addr ): await logged_gui.test_logout_and_switch_to_login_widget() - await aqtbot.run(logged_gui.add_instance, user_invitation_addr.to_url()) + logged_gui.add_instance(user_invitation_addr.to_url()) cd_w = await catch_claim_user_widget() assert cd_w assert logged_gui.tab_center.count() == 1 diff --git a/tests/core/gui/users_widget/test_claim.py b/tests/core/gui/users_widget/test_claim.py index 9e5aa421022..e581b3ed1ad 100644 --- a/tests/core/gui/users_widget/test_claim.py +++ b/tests/core/gui/users_widget/test_claim.py @@ -96,7 +96,7 @@ async def bootstrap(self): # Switch to users claim page - await aqtbot.run(gui.add_instance, invitation_addr.to_url()) + gui.add_instance(invitation_addr.to_url()) cu_w = await catch_claim_user_widget() assert isinstance(cu_w, ClaimUserWidget) @@ -190,7 +190,7 @@ async def step_3_exchange_greeter_sas(self): cuce_w = self.claim_user_code_exchange_widget # Pretend we have choosen the right code - await aqtbot.run(cuce_w.code_input_widget.good_code_clicked.emit) + cuce_w.code_input_widget.good_code_clicked.emit() self.greeter_in_progress_ctx = await self.greeter_in_progress_ctx.do_wait_peer_trust() claimer_sas = self.greeter_in_progress_ctx.claimer_sas @@ -233,7 +233,7 @@ async def step_5_provide_claim_info(self): await aqtbot.key_clicks(cupi_w.line_edit_user_email, human_email) await aqtbot.key_clicks(cupi_w.line_edit_user_full_name, human_label) - await aqtbot.run(cupi_w.line_edit_device.clear) + cupi_w.line_edit_device.clear() await aqtbot.key_clicks(cupi_w.line_edit_device, device_label) await aqtbot.mouse_click(cupi_w.button_ok, QtCore.Qt.LeftButton) @@ -365,7 +365,7 @@ async def offline_step_3_exchange_greeter_sas(self): with running_backend.offline(): assert not autoclose_dialog.dialogs - await aqtbot.run(cuce_w.code_input_widget.good_code_clicked.emit) + cuce_w.code_input_widget.good_code_clicked.emit() await aqtbot.wait_until(partial(self._claim_aborted, expected_message)) return None @@ -386,7 +386,7 @@ async def offline_step_5_provide_claim_info(self): with running_backend.offline(): await aqtbot.key_clicks(cupi_w.line_edit_user_email, human_email) await aqtbot.key_clicks(cupi_w.line_edit_user_full_name, human_label) - await aqtbot.run(cupi_w.line_edit_device.clear) + cupi_w.line_edit_device.clear() await aqtbot.key_clicks(cupi_w.line_edit_device, device_label) await aqtbot.mouse_click(cupi_w.button_ok, QtCore.Qt.LeftButton) await aqtbot.wait_until(partial(self._claim_aborted, expected_message)) @@ -442,7 +442,7 @@ async def reset_step_3_exchange_greeter_sas(self): cuce_w = self.claim_user_code_exchange_widget async with self._reset_greeter(): - await aqtbot.run(cuce_w.code_input_widget.good_code_clicked.emit) + cuce_w.code_input_widget.good_code_clicked.emit() await aqtbot.wait_until(partial(self._claim_restart, expected_message)) await self.bootstrap_after_restart() @@ -466,7 +466,7 @@ async def reset_step_5_provide_claim_info(self): async with self._reset_greeter(): await aqtbot.key_clicks(cupi_w.line_edit_user_email, human_email) await aqtbot.key_clicks(cupi_w.line_edit_user_full_name, human_label) - await aqtbot.run(cupi_w.line_edit_device.clear) + cupi_w.line_edit_device.clear() await aqtbot.key_clicks(cupi_w.line_edit_device, device_label) await aqtbot.mouse_click(cupi_w.button_ok, QtCore.Qt.LeftButton) await aqtbot.wait_until(partial(self._claim_restart, expected_message)) @@ -545,7 +545,7 @@ async def cancelled_step_3_exchange_greeter_sas(self): cuce_w = self.claim_user_code_exchange_widget await self._cancel_invitation() - await aqtbot.run(cuce_w.code_input_widget.good_code_clicked.emit) + cuce_w.code_input_widget.good_code_clicked.emit() await aqtbot.wait_until(partial(self._claim_restart, expected_message)) return None @@ -569,7 +569,7 @@ async def cancelled_step_5_provide_claim_info(self): await aqtbot.key_clicks(cupi_w.line_edit_user_email, human_email) await aqtbot.key_clicks(cupi_w.line_edit_user_full_name, human_label) - await aqtbot.run(cupi_w.line_edit_device.clear) + cupi_w.line_edit_device.clear() await aqtbot.key_clicks(cupi_w.line_edit_device, device_label) await aqtbot.mouse_click(cupi_w.button_ok, QtCore.Qt.LeftButton) await aqtbot.wait_until(partial(self._claim_restart, expected_message)) @@ -617,7 +617,7 @@ async def test_claim_user_already_deleted( reason=InvitationDeletedReason.CANCELLED, ) - await aqtbot.run(gui.add_instance, invitation_addr.to_url()) + gui.add_instance(invitation_addr.to_url()) def _assert_dialogs(): assert len(autoclose_dialog.dialogs) == 1 @@ -642,7 +642,7 @@ async def test_claim_user_offline_backend( token=invitation.token, ) with running_backend.offline(): - await aqtbot.run(gui.add_instance, invitation_addr.to_url()) + gui.add_instance(invitation_addr.to_url()) def _assert_dialogs(): assert len(autoclose_dialog.dialogs) == 1 @@ -666,7 +666,7 @@ async def test_claim_user_unknown_invitation( token=uuid4(), ) - await aqtbot.run(gui.add_instance, invitation_addr.to_url()) + gui.add_instance(invitation_addr.to_url()) def _assert_dialogs(): assert len(autoclose_dialog.dialogs) == 1 diff --git a/tests/core/gui/users_widget/test_greet.py b/tests/core/gui/users_widget/test_greet.py index bef8a629d19..4333d4ef125 100644 --- a/tests/core/gui/users_widget/test_greet.py +++ b/tests/core/gui/users_widget/test_greet.py @@ -219,7 +219,7 @@ async def step_4_exchange_claimer_sas(self): guce_w = self.greet_user_code_exchange_widget # Pretent we have clicked on the right choice - await aqtbot.run(guce_w.code_input_widget.good_code_clicked.emit) + guce_w.code_input_widget.good_code_clicked.emit() self.claimer_in_progress_ctx = await self.claimer_in_progress_ctx.do_wait_peer_trust() @@ -358,7 +358,7 @@ async def offline_step_4_exchange_claimer_sas(self): guce_w = self.greet_user_code_exchange_widget with running_backend.offline(): - await aqtbot.run(guce_w.code_input_widget.good_code_clicked.emit) + guce_w.code_input_widget.good_code_clicked.emit() await aqtbot.wait_until(partial(self._greet_aborted, expected_message)) return None @@ -427,7 +427,7 @@ async def reset_step_4_exchange_claimer_sas(self): guce_w = self.greet_user_code_exchange_widget # Pretent we have click on the right choice - await aqtbot.run(guce_w.code_input_widget.good_code_clicked.emit) + guce_w.code_input_widget.good_code_clicked.emit() async with self._reset_claimer(): await aqtbot.wait_until(partial(self._greet_restart, expected_message)) @@ -527,7 +527,7 @@ async def cancelled_step_4_exchange_claimer_sas(self): await self._cancel_invitation() - await aqtbot.run(guce_w.code_input_widget.good_code_clicked.emit) + guce_w.code_input_widget.good_code_clicked.emit() await aqtbot.wait_until(partial(self._greet_restart, expected_message)) return None diff --git a/tests/core/gui/users_widget/test_invite.py b/tests/core/gui/users_widget/test_invite.py index 1d6408c92a3..cda0c568e2f 100644 --- a/tests/core/gui/users_widget/test_invite.py +++ b/tests/core/gui/users_widget/test_invite.py @@ -104,7 +104,7 @@ async def test_revoke_user( ) if online: - await aqtbot.run(bob_w.revoke_clicked.emit, bob_w.user_info) + bob_w.revoke_clicked.emit(bob_w.user_info) def _revocation_done(): assert bob_w.user_info.is_revoked is True @@ -119,7 +119,7 @@ def _revocation_done(): else: with running_backend.offline(): - await aqtbot.run(bob_w.revoke_clicked.emit, bob_w.user_info) + bob_w.revoke_clicked.emit(bob_w.user_info) def _revocation_error(): assert bob_w.user_info.is_revoked is False @@ -149,7 +149,7 @@ async def test_revoke_user_not_allowed( lambda *args, **kwargs: _("ACTION_USER_REVOCATION_CONFIRM"), ) - await aqtbot.run(alice_w.revoke_clicked.emit, alice_w.user_info) + alice_w.revoke_clicked.emit(alice_w.user_info) def _revocation_error(): assert alice_w.user_info.is_revoked is False From 39c525299bf8584aea2206d3926062d135be7c74 Mon Sep 17 00:00:00 2001 From: Vincent Michel Date: Tue, 27 Jul 2021 15:20:02 +0200 Subject: [PATCH 18/35] Make sure ParsecApp is the default qt application --- parsec/core/gui/app.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/parsec/core/gui/app.py b/parsec/core/gui/app.py index 671dc96e36d..03bdd9522b4 100644 --- a/parsec/core/gui/app.py +++ b/parsec/core/gui/app.py @@ -141,11 +141,17 @@ def run_gui(config: CoreConfig, start_arg: Optional[str] = None, diagnose: bool QApplication.setHighDpiScaleFactorRoundingPolicy( Qt.HighDpiScaleFactorRoundingPolicy.PassThrough ) - return qtrio.run(_run_gui, config, start_arg, diagnose) - -async def _run_gui(config: CoreConfig, start_arg: str = None, diagnose: bool = False): + # The parsec app needs to be instanciated before qtrio runs in order + # to be the default QApplication instance app = ParsecApp() + assert QApplication.instance() is app + return qtrio.run(_run_gui, app, config, start_arg, diagnose) + + +async def _run_gui( + app: ParsecApp, config: CoreConfig, start_arg: str = None, diagnose: bool = False +): app.load_stylesheet() app.load_font() From bce8a2d2dd3230c4ae9b24f8aa9e823a74f93924 Mon Sep 17 00:00:00 2001 From: Vincent Michel Date: Tue, 27 Jul 2021 15:21:32 +0200 Subject: [PATCH 19/35] Make sure the GUI use a single point of truth for the mountpoint states --- parsec/core/gui/workspaces_widget.py | 32 +++++++++------------------- 1 file changed, 10 insertions(+), 22 deletions(-) diff --git a/parsec/core/gui/workspaces_widget.py b/parsec/core/gui/workspaces_widget.py index 1b2e99e8a48..c8947af669b 100644 --- a/parsec/core/gui/workspaces_widget.py +++ b/parsec/core/gui/workspaces_widget.py @@ -168,8 +168,7 @@ class WorkspacesWidget(QWidget, Ui_WorkspacesWidget): workspace_reencryption_success = pyqtSignal(QtToTrioJob) workspace_reencryption_error = pyqtSignal(QtToTrioJob) workspace_reencryption_progress = pyqtSignal(EntryID, int, int) - mountpoint_started = pyqtSignal(object, object) - mountpoint_stopped = pyqtSignal(object, object) + mountpoint_state_updated = pyqtSignal(object, object) rename_success = pyqtSignal(QtToTrioJob) rename_error = pyqtSignal(QtToTrioJob) @@ -235,8 +234,7 @@ def __init__(self, core, jobs_ctx, event_bus, **kwargs): self.filter_remove_button.clicked.connect(self.remove_user_filter) self.filter_remove_button.apply_style() - self.mountpoint_started.connect(self._on_mountpoint_started_qt) - self.mountpoint_stopped.connect(self._on_mountpoint_stopped_qt) + self.mountpoint_state_updated.connect(self._on_mountpoint_state_updated_qt) self.sharing_updated_qt.connect(self._on_sharing_updated_qt) self._workspace_created_qt.connect(self._on_workspace_created_qt) @@ -521,11 +519,7 @@ def open_workspace_file(self, workspace_fs, file_name): file_name = FsPath("/", file_name) if file_name else FsPath("/") try: - # The Qt thread should never hit the core directly. - # Synchronous calls can run directly in the job system - # as they won't block the Qt loop for long - path = self.jobs_ctx.run_sync( - self.core.mountpoint_manager.get_path_in_mountpoint, + path = self.core.mountpoint_manager.get_path_in_mountpoint( workspace_fs.workspace_id, file_name, workspace_fs.timestamp @@ -582,16 +576,14 @@ def update_workspace_config(self, workspace_id, state): ) def is_workspace_mounted(self, workspace_id, timestamp=None): - return self.jobs_ctx.run_sync( - self.core.mountpoint_manager.is_workspace_mounted, workspace_id, timestamp - ) + return self.core.mountpoint_manager.is_workspace_mounted(workspace_id, timestamp) def delete_workspace(self, workspace_fs): if isinstance(workspace_fs, WorkspaceFSTimestamped): self.unmount_workspace(workspace_fs.workspace_id, workspace_fs.timestamp) return else: - workspace_name = self.jobs_ctx.run_sync(workspace_fs.get_workspace_name) + workspace_name = workspace_fs.get_workspace_name() result = ask_question( self, _("TEXT_WORKSPACE_DELETE_TITLE"), @@ -807,18 +799,14 @@ def _on_fs_synced_qt(self, event, id): def _on_fs_updated_qt(self, event, workspace_id): self.reset() - def _on_mountpoint_started_qt(self, workspace_id, timestamp): - wb = self.get_workspace_button(workspace_id, timestamp) - if wb: - wb.set_mountpoint_state(True) - - def _on_mountpoint_stopped_qt(self, workspace_id, timestamp): + def _on_mountpoint_state_updated_qt(self, workspace_id, timestamp): wb = self.get_workspace_button(workspace_id, timestamp) if wb: - wb.set_mountpoint_state(False) + mounted = self.is_workspace_mounted(workspace_id, timestamp) + wb.set_mountpoint_state(mounted) def _on_mountpoint_started_trio(self, event, mountpoint, workspace_id, timestamp): - self.mountpoint_started.emit(workspace_id, timestamp) + self.mountpoint_state_updated.emit(workspace_id, timestamp) def _on_mountpoint_stopped_trio(self, event, mountpoint, workspace_id, timestamp): - self.mountpoint_stopped.emit(workspace_id, timestamp) + self.mountpoint_state_updated.emit(workspace_id, timestamp) From f85c9cd86bbf914f6ac95fb78b052ce6adf805ab Mon Sep 17 00:00:00 2001 From: Vincent Michel Date: Tue, 27 Jul 2021 15:23:05 +0200 Subject: [PATCH 20/35] Fix a race condition in the mountpoint runner In particuler, make sure the events are sent AFTER the inner state is mutated. --- parsec/core/core_events.py | 3 +- parsec/core/mountpoint/fuse_runner.py | 3 +- parsec/core/mountpoint/manager.py | 49 +++++++++++++++++++++---- parsec/core/mountpoint/winfsp_runner.py | 4 +- 4 files changed, 46 insertions(+), 13 deletions(-) diff --git a/parsec/core/core_events.py b/parsec/core/core_events.py index 98b2b2dc458..19be99a64e1 100644 --- a/parsec/core/core_events.py +++ b/parsec/core/core_events.py @@ -26,8 +26,9 @@ class CoreEvent(Enum): GUI_CONFIG_CHANGED = "gui.config.changed" # Mountpoint MOUNTPOINT_REMOTE_ERROR = "mountpoint.remote_error" - MOUNTPOINT_STARTED = "mountpoint.started" MOUNTPOINT_STARTING = "mountpoint.starting" + MOUNTPOINT_STARTED = "mountpoint.started" + MOUNTPOINT_STOPPING = "mountpoint.stopping" MOUNTPOINT_STOPPED = "mountpoint.stopped" MOUNTPOINT_UNHANDLED_ERROR = "mountpoint.unhandled_error" # Others diff --git a/parsec/core/mountpoint/fuse_runner.py b/parsec/core/mountpoint/fuse_runner.py index 3b9dbbb431b..33ae641785d 100644 --- a/parsec/core/mountpoint/fuse_runner.py +++ b/parsec/core/mountpoint/fuse_runner.py @@ -193,15 +193,14 @@ def _run_fuse_thread(): ) await _wait_for_fuse_ready(mountpoint_path, fuse_thread_started, initial_st_dev) - event_bus.send(CoreEvent.MOUNTPOINT_STARTED, **event_kwargs) task_status.started(mountpoint_path) finally: + event_bus.send(CoreEvent.MOUNTPOINT_STOPPING, **event_kwargs) with trio.CancelScope(shield=True) as teardown_cancel_scope: await _stop_fuse_thread( mountpoint_path, fuse_operations, fuse_thread_started, fuse_thread_stopped ) - event_bus.send(CoreEvent.MOUNTPOINT_STOPPED, **event_kwargs) await _teardown_mountpoint(mountpoint_path) diff --git a/parsec/core/mountpoint/manager.py b/parsec/core/mountpoint/manager.py index 70d65d50f69..ac3cbd45288 100644 --- a/parsec/core/mountpoint/manager.py +++ b/parsec/core/mountpoint/manager.py @@ -14,9 +14,9 @@ from async_generator import asynccontextmanager -from parsec.utils import start_task from parsec.core.types import FsPath, EntryID from parsec.core.fs.workspacefs import WorkspaceFSTimestamped +from parsec.utils import TaskStatus, start_task, open_service_nursery from parsec.core.fs.exceptions import FSWorkspaceNotFoundError, FSWorkspaceTimestampedTooEarly from parsec.core.mountpoint.exceptions import ( MountpointConfigurationError, @@ -141,12 +141,19 @@ async def _load_workspace_timestamped( self._timestamped_workspacefs[workspace_id] = {timestamp: new_workspace} return new_workspace - async def _mount_workspace_helper(self, workspace_fs, timestamp: DateTime = None): + async def _mount_workspace_helper(self, workspace_fs, timestamp: DateTime = None) -> TaskStatus: + finished = False + runner_task = None key = (workspace_fs.workspace_id, timestamp) + event_kwargs = { + "mountpoint": None, + "workspace_id": workspace_fs.workspace_id, + "timestamp": timestamp, + } async def curried_runner(task_status=trio.TASK_STATUS_IGNORED): try: - return await self._runner( + await self._runner( self.user_fs, workspace_fs, self.base_mountpoint_path, @@ -155,10 +162,36 @@ async def curried_runner(task_status=trio.TASK_STATUS_IGNORED): task_status=task_status, ) finally: - self._mountpoint_tasks.pop(key, None) - + # Tag the task as finished before cleaning up + nonlocal finished + finished = True + # The task failed before it started + if runner_task is None: + pass + # Another task has already been set for this mountpoint + elif self._mountpoint_tasks.get(key) != runner_task: + pass + # Set the mountpoint as unmounted THEN send the corresponding event + else: + self._mountpoint_tasks.pop(key, None) + self.event_bus.send(CoreEvent.MOUNTPOINT_STOPPED, **event_kwargs) + + # Start the mountpoint runner task runner_task = await start_task(self._nursery, curried_runner) - self._mountpoint_tasks[key] = runner_task + event_kwargs["mountpoint"] = runner_task.value + + # Another task has already been set for this mountpoint + if key in self._mountpoint_tasks: + await runner_task.cancel_and_join() + # The task somehow finished before we could register it + elif finished: + await runner_task.cancel_and_join() + # Set the mountpoint as mounted THEN send the corresponding event + else: + self._mountpoint_tasks[key] = runner_task + self.event_bus.send(CoreEvent.MOUNTPOINT_STARTED, **event_kwargs) + + # Return the task so the caller can access the path return runner_task def get_path_in_mountpoint( @@ -347,7 +380,7 @@ def on_event(event, new_entry, previous_entry=None): return # Instantiate the mountpoint manager with its own nursery - async with trio.open_service_nursery() as nursery: + async with open_service_nursery() as nursery: mountpoint_manager = MountpointManager( user_fs, event_bus, base_mountpoint_path, config, runner, nursery ) @@ -356,7 +389,7 @@ def on_event(event, new_entry, previous_entry=None): try: # A nursery dedicated to new workspace events - async with trio.open_service_nursery() as mount_nursery: + async with open_service_nursery() as mount_nursery: # Setup new workspace events with event_bus.connect_in_context( diff --git a/parsec/core/mountpoint/winfsp_runner.py b/parsec/core/mountpoint/winfsp_runner.py index 32ff57b977e..c05cbfa8fba 100644 --- a/parsec/core/mountpoint/winfsp_runner.py +++ b/parsec/core/mountpoint/winfsp_runner.py @@ -196,7 +196,6 @@ async def winfsp_mountpoint_runner( await _wait_for_winfsp_ready(mountpoint_path) # Notify the manager that the mountpoint is ready - event_bus.send(CoreEvent.MOUNTPOINT_STARTED, **event_kwargs) task_status.started(mountpoint_path) # Start recording `sharing.updated` events @@ -223,8 +222,9 @@ async def winfsp_mountpoint_runner( raise MountpointDriverCrash(f"WinFSP has crashed on {mountpoint_path}: {exc}") from exc finally: + event_bus.send(CoreEvent.MOUNTPOINT_STOPPING, **event_kwargs) + # Must run in thread given this call will wait for any winfsp operation # to finish so blocking the trio loop can produce a dead lock... with trio.CancelScope(shield=True): await trio.to_thread.run_sync(fs.stop) - event_bus.send(CoreEvent.MOUNTPOINT_STOPPED, **event_kwargs) From a3103acf163f646f63f9bb81a756ff6977db05d3 Mon Sep 17 00:00:00 2001 From: Vincent Michel Date: Tue, 27 Jul 2021 15:32:51 +0200 Subject: [PATCH 21/35] Remove QtToTrioJobScheduler.run_sync method --- parsec/core/gui/central_widget.py | 2 +- parsec/core/gui/files_widget.py | 15 ++++++--------- parsec/core/gui/greet_user_widget.py | 2 +- parsec/core/gui/trio_thread.py | 5 ----- parsec/core/gui/workspace_sharing_widget.py | 4 ++-- 5 files changed, 10 insertions(+), 18 deletions(-) diff --git a/parsec/core/gui/central_widget.py b/parsec/core/gui/central_widget.py index 6f66adc0eb0..3a637db5895 100644 --- a/parsec/core/gui/central_widget.py +++ b/parsec/core/gui/central_widget.py @@ -369,7 +369,7 @@ def go_to_file_link(self, addr: BackendOrganizationFileLinkAddr, mount: bool = T if addr.organization_id != self.core.device.organization_id: raise GoToFileLinkBadOrganizationIDError try: - workspace = self.jobs_ctx.run_sync(self.core.user_fs.get_workspace, addr.workspace_id) + workspace = self.core.user_fs.get_workspace(addr.workspace_id) except FSWorkspaceNotFoundError as exc: raise GoToFileLinkBadWorkspaceIDError from exc try: diff --git a/parsec/core/gui/files_widget.py b/parsec/core/gui/files_widget.py index 4c323c81e98..3c61a7e5ad8 100644 --- a/parsec/core/gui/files_widget.py +++ b/parsec/core/gui/files_widget.py @@ -368,7 +368,7 @@ def set_workspace_fs( self.workspace_fs = wk_fs self.load(current_directory) - ws_entry = self.jobs_ctx.run_sync(self.workspace_fs.get_workspace_entry) + ws_entry = self.workspace_fs.get_workspace_entry() self.current_user_role = ws_entry.role self.label_role.setText(get_role_translation(self.current_user_role)) self.table_files.current_user_role = self.current_user_role @@ -390,14 +390,12 @@ def set_workspace_fs( # Sending the source_workspace name for paste text self.table_files.paste_status = PasteStatus( status=PasteStatus.Status.Enabled, - source_workspace=str( - self.jobs_ctx.run_sync(self.clipboard.source_workspace.get_workspace_name) - ), + source_workspace=str(self.clipboard.source_workspace.get_workspace_name()), ) self.reset(default_selection) def reset(self, default_selection=None): - workspace_name = self.jobs_ctx.run_sync(self.workspace_fs.get_workspace_name) + workspace_name = self.workspace_fs.get_workspace_name() # Reload without any delay self.reload(default_selection, delay=0) self.table_files.sortItems(0) @@ -408,7 +406,7 @@ def on_get_file_path_clicked(self): if len(files) != 1: return path = self.current_directory / files[0].name - addr = self.jobs_ctx.run_sync(self.workspace_fs.generate_file_link, path) + addr = self.workspace_fs.generate_file_link(path) desktop.copy_to_clipboard(addr.to_url()) show_info(self, _("TEXT_FILE_LINK_COPIED_TO_CLIPBOARD")) @@ -617,8 +615,7 @@ def open_file(self, file_name): # The Qt thread should never hit the core directly. # Synchronous calls can run directly in the job system # as they won't block the Qt loop for long - path = self.jobs_ctx.run_sync( - self.core.mountpoint_manager.get_path_in_mountpoint, + path = self.core.mountpoint_manager.get_path_in_mountpoint( self.workspace_fs.workspace_id, self.current_directory / file_name if file_name else self.current_directory, self.workspace_fs.timestamp @@ -903,7 +900,7 @@ def _on_folder_stat_success(self, job): self.filter_files(self.line_edit_search.text()) if default_selection and not file_found: show_error(self, _("TEXT_FILE_GOTO_LINK_NOT_FOUND")) - workspace_name = self.jobs_ctx.run_sync(self.workspace_fs.get_workspace_name) + workspace_name = self.workspace_fs.get_workspace_name() self.folder_changed.emit(str(workspace_name), str(self.current_directory)) def _on_folder_stat_error(self, job): diff --git a/parsec/core/gui/greet_user_widget.py b/parsec/core/gui/greet_user_widget.py index 61a52d079e2..5d09a70e245 100644 --- a/parsec/core/gui/greet_user_widget.py +++ b/parsec/core/gui/greet_user_widget.py @@ -609,7 +609,7 @@ def _goto_page3(self): current_page.setParent(None) # The organization's config value is already cached in the core's logic # so the GUI doesn't need to set the value in its own cache - organization_config = self.jobs_ctx.run_sync(self.core.get_organization_config) + organization_config = self.core.get_organization_config() page = GreetUserCheckInfoWidget( self.jobs_ctx, self.greeter, organization_config.user_profile_outsider_allowed ) diff --git a/parsec/core/gui/trio_thread.py b/parsec/core/gui/trio_thread.py index 53386a0b520..52d5edbbb70 100644 --- a/parsec/core/gui/trio_thread.py +++ b/parsec/core/gui/trio_thread.py @@ -195,11 +195,6 @@ def submit_job(self, qt_on_success, qt_on_error, fn, *args, **kwargs): self.nursery.start_soon(job) return job - def run_sync(self, fn, *args): - if self.nursery._closed: - raise JobSchedulerNotAvailable - return fn(*args) - @asynccontextmanager async def run_trio_job_scheduler(): diff --git a/parsec/core/gui/workspace_sharing_widget.py b/parsec/core/gui/workspace_sharing_widget.py index 6ae918e6f45..dd8a115a645 100644 --- a/parsec/core/gui/workspace_sharing_widget.py +++ b/parsec/core/gui/workspace_sharing_widget.py @@ -181,7 +181,7 @@ def __init__(self, user_fs, workspace_fs, core, jobs_ctx): self.get_users_error.connect(self._on_get_users_error) self.line_edit_filter.textChanged.connect(self._on_filter_changed) - ws_entry = self.jobs_ctx.run_sync(self.workspace_fs.get_workspace_entry) + ws_entry = self.workspace_fs.get_workspace_entry() self.current_user_role = ws_entry.role self.reset() @@ -328,7 +328,7 @@ def reset(self): @classmethod def show_modal(cls, user_fs, workspace_fs, core, jobs_ctx, parent, on_finished): - workspace_name = jobs_ctx.run_sync(workspace_fs.get_workspace_name) + workspace_name = workspace_fs.get_workspace_name() w = cls(user_fs=user_fs, workspace_fs=workspace_fs, core=core, jobs_ctx=jobs_ctx) d = GreyedDialog( From 8eb61185e67daf737f1656167ae33ba67fbd8eaa Mon Sep 17 00:00:00 2001 From: Vincent Michel Date: Wed, 28 Jul 2021 12:23:32 +0200 Subject: [PATCH 22/35] Refactor _mount_workspace_helper --- parsec/core/mountpoint/fuse_runner.py | 7 +-- parsec/core/mountpoint/manager.py | 66 +++++++++++-------------- parsec/core/mountpoint/winfsp_runner.py | 6 +-- parsec/utils.py | 21 ++++---- 4 files changed, 47 insertions(+), 53 deletions(-) diff --git a/parsec/core/mountpoint/fuse_runner.py b/parsec/core/mountpoint/fuse_runner.py index 33ae641785d..e408c9e09e3 100644 --- a/parsec/core/mountpoint/fuse_runner.py +++ b/parsec/core/mountpoint/fuse_runner.py @@ -12,6 +12,7 @@ from fuse import FUSE from structlog import get_logger from contextlib import contextmanager +from async_generator import asynccontextmanager from itertools import count from parsec.event_bus import EventBus @@ -89,14 +90,13 @@ async def _teardown_mountpoint(mountpoint_path): pass +@asynccontextmanager async def fuse_mountpoint_runner( user_fs: UserFS, workspace_fs: WorkspaceFS, base_mountpoint_path: PurePath, config: dict, event_bus: EventBus, - *, - task_status=trio.TASK_STATUS_IGNORED, ): """ Raises: @@ -193,7 +193,8 @@ def _run_fuse_thread(): ) await _wait_for_fuse_ready(mountpoint_path, fuse_thread_started, initial_st_dev) - task_status.started(mountpoint_path) + # Indicate the mountpoint is now started + yield mountpoint_path finally: event_bus.send(CoreEvent.MOUNTPOINT_STOPPING, **event_kwargs) diff --git a/parsec/core/mountpoint/manager.py b/parsec/core/mountpoint/manager.py index ac3cbd45288..262dcd79da2 100644 --- a/parsec/core/mountpoint/manager.py +++ b/parsec/core/mountpoint/manager.py @@ -142,56 +142,48 @@ async def _load_workspace_timestamped( return new_workspace async def _mount_workspace_helper(self, workspace_fs, timestamp: DateTime = None) -> TaskStatus: - finished = False - runner_task = None - key = (workspace_fs.workspace_id, timestamp) - event_kwargs = { - "mountpoint": None, - "workspace_id": workspace_fs.workspace_id, - "timestamp": timestamp, - } + workspace_id = workspace_fs.workspace_id + key = workspace_id, timestamp - async def curried_runner(task_status=trio.TASK_STATUS_IGNORED): + async def curried_runner(task_status): + event_kwargs = {} try: - await self._runner( + async with self._runner( self.user_fs, workspace_fs, self.base_mountpoint_path, config=self.config, event_bus=self.event_bus, - task_status=task_status, - ) + ) as mountpoint_path: + + # Another runner started before us + if key in self._mountpoint_tasks: + raise MountpointAlreadyMounted( + f"Workspace `{workspace_id}` already mounted." + ) + + # Prepare kwargs for both started and stopped events + event_kwargs = { + "mountpoint": mountpoint_path, + "workspace_id": workspace_fs.workspace_id, + "timestamp": timestamp, + } + + # Set the mountpoint as mounted THEN send the corresponding event + task_status.started(mountpoint_path) + self._mountpoint_tasks[key] = task_status + self.event_bus.send(CoreEvent.MOUNTPOINT_STARTED, **event_kwargs) + finally: - # Tag the task as finished before cleaning up - nonlocal finished - finished = True - # The task failed before it started - if runner_task is None: - pass - # Another task has already been set for this mountpoint - elif self._mountpoint_tasks.get(key) != runner_task: - pass - # Set the mountpoint as unmounted THEN send the corresponding event - else: + # Pop the mountpoint task if its ours + if self._mountpoint_tasks.get(key) == task_status: self._mountpoint_tasks.pop(key, None) + # Send stopped event if started has been previously sent + if event_kwargs: self.event_bus.send(CoreEvent.MOUNTPOINT_STOPPED, **event_kwargs) # Start the mountpoint runner task runner_task = await start_task(self._nursery, curried_runner) - event_kwargs["mountpoint"] = runner_task.value - - # Another task has already been set for this mountpoint - if key in self._mountpoint_tasks: - await runner_task.cancel_and_join() - # The task somehow finished before we could register it - elif finished: - await runner_task.cancel_and_join() - # Set the mountpoint as mounted THEN send the corresponding event - else: - self._mountpoint_tasks[key] = runner_task - self.event_bus.send(CoreEvent.MOUNTPOINT_STARTED, **event_kwargs) - - # Return the task so the caller can access the path return runner_task def get_path_in_mountpoint( diff --git a/parsec/core/mountpoint/winfsp_runner.py b/parsec/core/mountpoint/winfsp_runner.py index c05cbfa8fba..3042f5c984d 100644 --- a/parsec/core/mountpoint/winfsp_runner.py +++ b/parsec/core/mountpoint/winfsp_runner.py @@ -7,6 +7,7 @@ from pathlib import PurePath from functools import partial from structlog import get_logger +from async_generator import asynccontextmanager from winfspy import FileSystem, enable_debug_log from winfspy.plumbing import filetime_now @@ -106,14 +107,13 @@ async def _wait_for_winfsp_ready(mountpoint_path, timeout=1.0): return +@asynccontextmanager async def winfsp_mountpoint_runner( user_fs: UserFS, workspace_fs: WorkspaceFS, base_mountpoint_path: PurePath, config: dict, event_bus: EventBus, - *, - task_status=trio.TASK_STATUS_IGNORED, ): """ Raises: @@ -196,7 +196,7 @@ async def winfsp_mountpoint_runner( await _wait_for_winfsp_ready(mountpoint_path) # Notify the manager that the mountpoint is ready - task_status.started(mountpoint_path) + yield mountpoint_path # Start recording `sharing.updated` events with event_bus.waiter_on(CoreEvent.SHARING_UPDATED) as waiter: diff --git a/parsec/utils.py b/parsec/utils.py index 4bbd6231839..06c8148cd73 100644 --- a/parsec/utils.py +++ b/parsec/utils.py @@ -37,7 +37,7 @@ def timestamps_in_the_ballpark(ts1: DateTime, ts2: DateTime, max_dt=TIMESTAMP_MA class TaskStatus: # Internal state - + _trio_task_status = attr.ib() _cancel_scope = attr.ib(default=None) _started_value = attr.ib(default=None) _finished_event = attr.ib(factory=trio.Event) @@ -45,12 +45,15 @@ class TaskStatus: def _set_cancel_scope(self, scope): self._cancel_scope = scope - def _set_started_value(self, value): - self._started_value = value - def _set_finished(self): self._finished_event.set() + # Trio-like methods + + def started(self, value=None): + self._started_value = value + self._trio_task_status.started(self) + # Properties @property @@ -82,13 +85,11 @@ async def cancel_and_join(self): @classmethod async def wrap_task(cls, corofn, *args, task_status=trio.TASK_STATUS_IGNORED): - status = cls() + status = cls(task_status) try: - async with trio.open_service_nursery() as nursery: - status._set_cancel_scope(nursery.cancel_scope) - value = await nursery.start(corofn, *args) - status._set_started_value(value) - task_status.started(status) + with trio.CancelScope() as cancel_scope: + status._set_cancel_scope(cancel_scope) + await corofn(*args, task_status=status) finally: status._set_finished() From 113cffa9dc4e9a53353bf7189cc19dcec37e3f9e Mon Sep 17 00:00:00 2001 From: Vincent Michel Date: Thu, 29 Jul 2021 11:29:45 +0200 Subject: [PATCH 23/35] Simplify the _run_ipc_server routine --- .../{1711.misc.rst => 1771.empty.rst} | 0 parsec/core/gui/app.py | 90 +++++++++---------- 2 files changed, 42 insertions(+), 48 deletions(-) rename newsfragments/{1711.misc.rst => 1771.empty.rst} (100%) diff --git a/newsfragments/1711.misc.rst b/newsfragments/1771.empty.rst similarity index 100% rename from newsfragments/1711.misc.rst rename to newsfragments/1771.empty.rst diff --git a/parsec/core/gui/app.py b/parsec/core/gui/app.py index 03bdd9522b4..e502aa56f14 100644 --- a/parsec/core/gui/app.py +++ b/parsec/core/gui/app.py @@ -4,7 +4,6 @@ import signal from typing import Optional from contextlib import contextmanager -from enum import Enum import trio import qtrio @@ -48,53 +47,47 @@ def _before_quit(): return _before_quit -IPCServerStartupOutcome = Enum("IPCServerStartupOutcome", "STARTED ALREADY_RUNNING ERROR") - - async def _run_ipc_server(config, main_window, start_arg, task_status=trio.TASK_STATUS_IGNORED): - try: - new_instance_needed_qt = main_window.new_instance_needed - foreground_needed_qt = main_window.foreground_needed + new_instance_needed_qt = main_window.new_instance_needed + foreground_needed_qt = main_window.foreground_needed + + async def _cmd_handler(cmd): + if cmd["cmd"] == IPCCommand.FOREGROUND: + foreground_needed_qt.emit() + elif cmd["cmd"] == IPCCommand.NEW_INSTANCE: + new_instance_needed_qt.emit(cmd.get("start_arg")) + return {"status": "ok"} + + # Loop over attemps at running an IPC server or sending the command to an existing one + while True: + + # Attempt to run an IPC server if Parsec is not already started + try: + async with run_ipc_server( + _cmd_handler, config.ipc_socket_file, win32_mutex_name=config.ipc_win32_mutex_name + ): + task_status.started() + await trio.sleep_forever() - async def _cmd_handler(cmd): - if cmd["cmd"] == IPCCommand.FOREGROUND: - foreground_needed_qt.emit() - elif cmd["cmd"] == IPCCommand.NEW_INSTANCE: - new_instance_needed_qt.emit(cmd.get("start_arg")) - return {"status": "ok"} + # Parsec is already started, give it our work then + except IPCServerAlreadyRunning: - while True: + # Protect against race conditions, in case the server was shutting down try: - async with run_ipc_server( - _cmd_handler, - config.ipc_socket_file, - win32_mutex_name=config.ipc_win32_mutex_name, - ): - task_status.started(IPCServerStartupOutcome.STARTED) - await trio.sleep_forever() - - except IPCServerAlreadyRunning: - # Parsec is already started, give it our work then - try: - if start_arg: - await send_to_ipc_server( - config.ipc_socket_file, IPCCommand.NEW_INSTANCE, start_arg=start_arg - ) - else: - await send_to_ipc_server(config.ipc_socket_file, IPCCommand.FOREGROUND) - - except IPCServerNotRunning: - # IPC server has closed, retry to create our own - continue - - # We have successfuly noticed the other running application - task_status.started(IPCServerStartupOutcome.ALREADY_RUNNING) - break - - except Exception as exc: - task_status.started(IPCServerStartupOutcome.ERROR) - logger.exection(exc) - raise + if start_arg: + await send_to_ipc_server( + config.ipc_socket_file, IPCCommand.NEW_INSTANCE, start_arg=start_arg + ) + else: + await send_to_ipc_server(config.ipc_socket_file, IPCCommand.FOREGROUND) + + # IPC server has closed, retry to create our own + except IPCServerNotRunning: + continue + + # We have successfuly noticed the other running application + # We can now forward the exception to the caller + raise @contextmanager @@ -167,10 +160,11 @@ async def _run_gui( minimize_on_close=config.gui_tray_enabled and systray_available(), ) - result = await jobs_ctx.nursery.start(_run_ipc_server, config, win, start_arg) - - if result == IPCServerStartupOutcome.ALREADY_RUNNING: - # Another instance of Parsec already started, nothing more to do + # Attempt to run an IPC server if Parsec is not already started + try: + await jobs_ctx.nursery.start(_run_ipc_server, config, win, start_arg) + # Another instance of Parsec already started, nothing more to do + except IPCServerAlreadyRunning: return # If we are here, it's either the IPC server has successfully started From 77b36ca07c3ea87139e46f91f9e6d5f91a70c831 Mon Sep 17 00:00:00 2001 From: Vincent Michel Date: Thu, 29 Jul 2021 11:35:56 +0200 Subject: [PATCH 24/35] Address a few of @touilleMan's comments --- parsec/core/mountpoint/manager.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/parsec/core/mountpoint/manager.py b/parsec/core/mountpoint/manager.py index 262dcd79da2..9c3a62d930b 100644 --- a/parsec/core/mountpoint/manager.py +++ b/parsec/core/mountpoint/manager.py @@ -145,8 +145,9 @@ async def _mount_workspace_helper(self, workspace_fs, timestamp: DateTime = None workspace_id = workspace_fs.workspace_id key = workspace_id, timestamp - async def curried_runner(task_status): + async def curried_runner(task_status: TaskStatus): event_kwargs = {} + try: async with self._runner( self.user_fs, @@ -177,7 +178,7 @@ async def curried_runner(task_status): finally: # Pop the mountpoint task if its ours if self._mountpoint_tasks.get(key) == task_status: - self._mountpoint_tasks.pop(key, None) + del self._mountpoint_tasks[key] # Send stopped event if started has been previously sent if event_kwargs: self.event_bus.send(CoreEvent.MOUNTPOINT_STOPPED, **event_kwargs) From 4ccd22035fe7eb884eebe4b08f6d3437ed0a0cc5 Mon Sep 17 00:00:00 2001 From: Vincent Michel Date: Thu, 29 Jul 2021 11:51:47 +0200 Subject: [PATCH 25/35] Remove await before sync aqtbot methods --- tests/core/gui/conftest.py | 29 ++-- tests/core/gui/devices_widget/test_claim.py | 42 +++-- tests/core/gui/devices_widget/test_greet.py | 8 +- tests/core/gui/devices_widget/test_invite.py | 14 +- tests/core/gui/test_code_widget.py | 6 +- tests/core/gui/test_create_organization.py | 158 +++++++++---------- tests/core/gui/test_custom_widgets.py | 2 +- tests/core/gui/test_expired_organization.py | 8 +- tests/core/gui/test_file_table.py | 8 +- tests/core/gui/test_files.py | 16 +- tests/core/gui/test_input_dialogs.py | 10 +- tests/core/gui/test_keys.py | 4 +- tests/core/gui/test_loading_dialog.py | 4 +- tests/core/gui/test_login.py | 18 +-- tests/core/gui/test_main_window.py | 14 +- tests/core/gui/test_menu.py | 8 +- tests/core/gui/test_password_change.py | 26 +-- tests/core/gui/test_password_validation.py | 6 +- tests/core/gui/test_validators.py | 6 +- tests/core/gui/test_workspace_button.py | 18 +-- tests/core/gui/test_workspace_sharing.py | 20 +-- tests/core/gui/test_workspaces.py | 14 +- tests/core/gui/test_workspaces_reencrypt.py | 14 +- tests/core/gui/users_widget/test_claim.py | 44 +++--- tests/core/gui/users_widget/test_greet.py | 16 +- tests/core/gui/users_widget/test_invite.py | 10 +- tests/core/gui/users_widget/test_list.py | 8 +- 27 files changed, 252 insertions(+), 279 deletions(-) diff --git a/tests/core/gui/conftest.py b/tests/core/gui/conftest.py index eb791f8aab3..d0dc626dd55 100644 --- a/tests/core/gui/conftest.py +++ b/tests/core/gui/conftest.py @@ -43,12 +43,7 @@ def __getattr__(self, name): words = name.split("_") camel_name = words[0] + "".join(word.title() for word in words[1:]) if hasattr(self.qtbot, camel_name): - - async def method(*args, **kwargs): - return getattr(self.qtbot, camel_name)(*args, **kwargs) - - return method - + return getattr(self.qtbot, camel_name) raise AttributeError(name) async def wait(self, timeout): @@ -250,7 +245,7 @@ async def _gui_factory( main_w = testing_main_window_cls( job_scheduler, job_scheduler.close, event_bus, core_config, minimize_on_close=True ) - aqtbot.qtbot.add_widget(main_w) + aqtbot.add_widget(main_w) main_w.show_window(skip_dialogs=skip_dialogs) main_w.show_top() windows.append(main_w) @@ -375,7 +370,7 @@ async def test_proceed_to_login(self, password, error=False): if isinstance(accounts_w, LoginAccountsWidget): async with aqtbot.wait_signal(accounts_w.account_clicked): - await aqtbot.mouse_click( + aqtbot.mouse_click( accounts_w.accounts_widget.layout().itemAt(0).widget(), QtCore.Qt.LeftButton ) @@ -384,11 +379,11 @@ def _password_widget_shown(): await aqtbot.wait_until(_password_widget_shown) password_w = l_w.widget.layout().itemAt(0).widget() - await aqtbot.key_clicks(password_w.line_edit_password, password) + aqtbot.key_clicks(password_w.line_edit_password, password) signal = tabw.logged_in if not error else tabw.login_failed async with aqtbot.wait_signals([l_w.login_with_password_clicked, signal]): - await aqtbot.mouse_click(password_w.button_login, QtCore.Qt.LeftButton) + aqtbot.mouse_click(password_w.button_login, QtCore.Qt.LeftButton) def _wait_logged_in(): assert not l_w.isVisible() @@ -404,7 +399,7 @@ async def test_switch_to_devices_widget(self, error=False): d_w = self.test_get_devices_widget() signal = d_w.list_error if error else d_w.list_success async with aqtbot.wait_exposed(d_w), aqtbot.wait_signal(signal, timeout=3000): - await aqtbot.mouse_click(central_widget.menu.button_devices, QtCore.Qt.LeftButton) + aqtbot.mouse_click(central_widget.menu.button_devices, QtCore.Qt.LeftButton) return d_w async def test_switch_to_users_widget(self, error=False): @@ -412,7 +407,7 @@ async def test_switch_to_users_widget(self, error=False): u_w = self.test_get_users_widget() signal = u_w.list_error if error else u_w.list_success async with aqtbot.wait_exposed(u_w), aqtbot.wait_signal(signal, timeout=3000): - await aqtbot.mouse_click(central_widget.menu.button_users, QtCore.Qt.LeftButton) + aqtbot.mouse_click(central_widget.menu.button_users, QtCore.Qt.LeftButton) return u_w async def test_switch_to_workspaces_widget(self, error=False): @@ -420,7 +415,7 @@ async def test_switch_to_workspaces_widget(self, error=False): w_w = self.test_get_workspaces_widget() signal = w_w.list_error if error else w_w.list_success async with aqtbot.wait_exposed(w_w), aqtbot.wait_signal(signal, timeout=3000): - await aqtbot.mouse_click(central_widget.menu.button_files, QtCore.Qt.LeftButton) + aqtbot.mouse_click(central_widget.menu.button_files, QtCore.Qt.LeftButton) return w_w async def test_switch_to_files_widget(self, workspace_name, error=False): @@ -438,7 +433,7 @@ async def test_switch_to_files_widget(self, workspace_name, error=False): # We need to make sure the workspace button is ready for left click first await aqtbot.wait_until(wk_button.switch_button.isChecked) # Send the click - await aqtbot.mouse_click(wk_button, QtCore.Qt.LeftButton) + aqtbot.mouse_click(wk_button, QtCore.Qt.LeftButton) # Wait for the spinner to disappear await aqtbot.wait_until(f_w.spinner.isHidden) @@ -460,7 +455,7 @@ async def test_switch_to_logged_in(self, device): if isinstance(accounts_w, LoginAccountsWidget): async with aqtbot.wait_signal(accounts_w.account_clicked): - await aqtbot.mouse_click( + aqtbot.mouse_click( accounts_w.accounts_widget.layout().itemAt(0).widget(), QtCore.Qt.LeftButton ) @@ -471,10 +466,10 @@ def _password_widget_shown(): password_w = lw.widget.layout().itemAt(0).widget() - await aqtbot.key_clicks(password_w.line_edit_password, "P@ssw0rd") + aqtbot.key_clicks(password_w.line_edit_password, "P@ssw0rd") async with aqtbot.wait_signals([lw.login_with_password_clicked, tabw.logged_in]): - await aqtbot.mouse_click(password_w.button_login, QtCore.Qt.LeftButton) + aqtbot.mouse_click(password_w.button_login, QtCore.Qt.LeftButton) central_widget = self.test_get_central_widget() assert central_widget is not None diff --git a/tests/core/gui/devices_widget/test_claim.py b/tests/core/gui/devices_widget/test_claim.py index 241f1925f4c..c2b0251ef15 100644 --- a/tests/core/gui/devices_widget/test_claim.py +++ b/tests/core/gui/devices_widget/test_claim.py @@ -144,7 +144,7 @@ def assert_initial_state(self): async def step_1_start_claim(self): cdi_w = self.claim_device_instructions_widget - await aqtbot.mouse_click(cdi_w.button_start, QtCore.Qt.LeftButton) + aqtbot.mouse_click(cdi_w.button_start, QtCore.Qt.LeftButton) def _claimer_started(): assert not cdi_w.button_start.isEnabled() @@ -226,13 +226,13 @@ async def step_5_provide_claim_info(self): assert not cdpi_w.button_ok.isEnabled() - await aqtbot.key_clicks(cdpi_w.line_edit_device, device_label) - await aqtbot.key_clicks(cdpi_w.widget_password.line_edit_password, self.password) - await aqtbot.key_clicks(cdpi_w.widget_password.line_edit_password_check, self.password) + aqtbot.key_clicks(cdpi_w.line_edit_device, device_label) + aqtbot.key_clicks(cdpi_w.widget_password.line_edit_password, self.password) + aqtbot.key_clicks(cdpi_w.widget_password.line_edit_password_check, self.password) assert cdpi_w.button_ok.isEnabled() - await aqtbot.mouse_click(cdpi_w.button_ok, QtCore.Qt.LeftButton) + aqtbot.mouse_click(cdpi_w.button_ok, QtCore.Qt.LeftButton) def _claim_info_submitted(): assert not cdpi_w.button_ok.isEnabled() @@ -306,7 +306,7 @@ async def offline_step_1_start_claim(self): cdi_w = self.claim_device_instructions_widget with running_backend.offline(): - await aqtbot.mouse_click(cdi_w.button_start, QtCore.Qt.LeftButton) + aqtbot.mouse_click(cdi_w.button_start, QtCore.Qt.LeftButton) await aqtbot.wait_until(partial(self._claim_aborted, expected_message)) return None @@ -342,12 +342,10 @@ async def offline_step_5_provide_claim_info(self): with running_backend.offline(): cdpi_w.line_edit_device.clear() - await aqtbot.key_clicks(cdpi_w.line_edit_device, device_label) - await aqtbot.key_clicks(cdpi_w.widget_password.line_edit_password, self.password) - await aqtbot.key_clicks( - cdpi_w.widget_password.line_edit_password_check, self.password - ) - await aqtbot.mouse_click(cdpi_w.button_ok, QtCore.Qt.LeftButton) + aqtbot.key_clicks(cdpi_w.line_edit_device, device_label) + aqtbot.key_clicks(cdpi_w.widget_password.line_edit_password, self.password) + aqtbot.key_clicks(cdpi_w.widget_password.line_edit_password_check, self.password) + aqtbot.mouse_click(cdpi_w.button_ok, QtCore.Qt.LeftButton) await aqtbot.wait_until(partial(self._claim_aborted, expected_message)) return None @@ -421,12 +419,10 @@ async def reset_step_5_provide_claim_info(self): async with self._reset_greeter(): cdpi_w.line_edit_device.clear() - await aqtbot.key_clicks(cdpi_w.line_edit_device, device_label) - await aqtbot.key_clicks(cdpi_w.widget_password.line_edit_password, self.password) - await aqtbot.key_clicks( - cdpi_w.widget_password.line_edit_password_check, self.password - ) - await aqtbot.mouse_click(cdpi_w.button_ok, QtCore.Qt.LeftButton) + aqtbot.key_clicks(cdpi_w.line_edit_device, device_label) + aqtbot.key_clicks(cdpi_w.widget_password.line_edit_password, self.password) + aqtbot.key_clicks(cdpi_w.widget_password.line_edit_password_check, self.password) + aqtbot.mouse_click(cdpi_w.button_ok, QtCore.Qt.LeftButton) await aqtbot.wait_until(partial(self._claim_restart, expected_message)) await self.bootstrap_after_restart() @@ -483,7 +479,7 @@ async def cancelled_step_1_start_claim(self): await self._cancel_invitation() - await aqtbot.mouse_click(cdi_w.button_start, QtCore.Qt.LeftButton) + aqtbot.mouse_click(cdi_w.button_start, QtCore.Qt.LeftButton) await aqtbot.wait_until(partial(self._claim_restart, expected_message)) return None @@ -522,10 +518,10 @@ async def cancelled_step_5_provide_claim_info(self): await self._cancel_invitation() cdpi_w.line_edit_device.clear() - await aqtbot.key_clicks(cdpi_w.line_edit_device, device_label) - await aqtbot.key_clicks(cdpi_w.widget_password.line_edit_password, self.password) - await aqtbot.key_clicks(cdpi_w.widget_password.line_edit_password_check, self.password) - await aqtbot.mouse_click(cdpi_w.button_ok, QtCore.Qt.LeftButton) + aqtbot.key_clicks(cdpi_w.line_edit_device, device_label) + aqtbot.key_clicks(cdpi_w.widget_password.line_edit_password, self.password) + aqtbot.key_clicks(cdpi_w.widget_password.line_edit_password_check, self.password) + aqtbot.mouse_click(cdpi_w.button_ok, QtCore.Qt.LeftButton) await aqtbot.wait_until(partial(self._claim_restart, expected_message)) return None diff --git a/tests/core/gui/devices_widget/test_greet.py b/tests/core/gui/devices_widget/test_greet.py index d3c9cdbc08e..7d48244e5c8 100644 --- a/tests/core/gui/devices_widget/test_greet.py +++ b/tests/core/gui/devices_widget/test_greet.py @@ -91,7 +91,7 @@ async def bootstrap(self): assert devices_widget.layout_devices.count() == 2 # Click on the invitation button - await aqtbot.mouse_click(devices_widget.button_add_device, QtCore.Qt.LeftButton) + aqtbot.mouse_click(devices_widget.button_add_device, QtCore.Qt.LeftButton) greet_device_widget = await catch_greet_device_widget() assert isinstance(greet_device_widget, GreetDeviceWidget) @@ -145,7 +145,7 @@ def assert_initial_state(self): async def step_1_start_greet(self): gdi_w = self.greet_device_information_widget - await aqtbot.mouse_click(gdi_w.button_start, QtCore.Qt.LeftButton) + aqtbot.mouse_click(gdi_w.button_start, QtCore.Qt.LeftButton) def _greet_started(): assert not gdi_w.button_start.isEnabled() @@ -276,7 +276,7 @@ async def offline_step_1_start_greet(self): gui_w = self.greet_device_information_widget with running_backend.offline(): - await aqtbot.mouse_click(gui_w.button_start, QtCore.Qt.LeftButton) + aqtbot.mouse_click(gui_w.button_start, QtCore.Qt.LeftButton) await aqtbot.wait_until(partial(self._greet_aborted, expected_message)) return None @@ -412,7 +412,7 @@ async def cancelled_step_1_start_greet(self): await self._cancel_invitation() - await aqtbot.mouse_click(gui_w.button_start, QtCore.Qt.LeftButton) + aqtbot.mouse_click(gui_w.button_start, QtCore.Qt.LeftButton) await aqtbot.wait_until(partial(self._greet_restart, expected_message)) return None diff --git a/tests/core/gui/devices_widget/test_invite.py b/tests/core/gui/devices_widget/test_invite.py index 60dd830bf5f..9f918739225 100644 --- a/tests/core/gui/devices_widget/test_invite.py +++ b/tests/core/gui/devices_widget/test_invite.py @@ -32,7 +32,7 @@ async def test_invite_device_offline(aqtbot, logged_gui, autoclose_dialog, runni # TODO: not sure why but this timeout without running_backend fixture... with running_backend.offline(): - await aqtbot.mouse_click(d_w.button_add_device, QtCore.Qt.LeftButton) + aqtbot.mouse_click(d_w.button_add_device, QtCore.Qt.LeftButton) def _invite_failed(): assert autoclose_dialog.dialogs == [ @@ -57,7 +57,7 @@ async def test_invite_device_send_email( ): d_w = await logged_gui.test_switch_to_devices_widget() - await aqtbot.mouse_click(d_w.button_add_device, QtCore.Qt.LeftButton) + aqtbot.mouse_click(d_w.button_add_device, QtCore.Qt.LeftButton) # Device invitation widget should show up now @@ -78,7 +78,7 @@ def _greet_device_displayed(): await aqtbot.wait_until(_greet_device_displayed) if online: - await aqtbot.mouse_click(gdi_w.button_send_email, QtCore.Qt.LeftButton) + aqtbot.mouse_click(gdi_w.button_send_email, QtCore.Qt.LeftButton) def _email_sent(): assert email_letterbox.emails == [(bob.human_handle.email, ANY)] @@ -88,7 +88,7 @@ def _email_sent(): else: with running_backend.offline(): - await aqtbot.mouse_click(gdi_w.button_send_email, QtCore.Qt.LeftButton) + aqtbot.mouse_click(gdi_w.button_send_email, QtCore.Qt.LeftButton) def _email_send_failed(): assert autoclose_dialog.dialogs == [("Error", "Could not send the email.")] @@ -105,7 +105,7 @@ async def test_invite_device_without_human_handle_cannot_send_email( ): d_w = await logged_gui.test_switch_to_devices_widget() - await aqtbot.mouse_click(d_w.button_add_device, QtCore.Qt.LeftButton) + aqtbot.mouse_click(d_w.button_add_device, QtCore.Qt.LeftButton) # Device invitation widget should show up now @@ -141,7 +141,7 @@ async def test_invite_and_greet_device( d_w = await logged_gui.test_switch_to_devices_widget() - await aqtbot.mouse_click(d_w.button_add_device, QtCore.Qt.LeftButton) + aqtbot.mouse_click(d_w.button_add_device, QtCore.Qt.LeftButton) # Device invitation widget should show up now with welcome page @@ -200,7 +200,7 @@ async def _run_claimer(): # Start the greeting - await aqtbot.mouse_click(gdi_w.button_start, QtCore.Qt.LeftButton) + aqtbot.mouse_click(gdi_w.button_start, QtCore.Qt.LeftButton) def _greet_started(): assert not gdi_w.button_start.isEnabled() diff --git a/tests/core/gui/test_code_widget.py b/tests/core/gui/test_code_widget.py index b2af413ca39..bf994c065b7 100644 --- a/tests/core/gui/test_code_widget.py +++ b/tests/core/gui/test_code_widget.py @@ -9,7 +9,7 @@ @pytest.mark.gui def test_code_input_right_choice(qtbot): w = CodeInputWidget() - qtbot.addWidget(w) + qtbot.add_widget(w) w.set_choices(choices=["A", "B", "C", "D"], right_choice="C") right_btn = None for i in range(w.code_layout.count()): @@ -28,7 +28,7 @@ def test_code_input_right_choice(qtbot): @pytest.mark.gui def test_code_input_wrong_choice(qtbot): w = CodeInputWidget() - qtbot.addWidget(w) + qtbot.add_widget(w) w.set_choices(choices=["A", "B", "C", "D"], right_choice="C") wrong_btn = None for i in range(w.code_layout.count()): @@ -47,7 +47,7 @@ def test_code_input_wrong_choice(qtbot): @pytest.mark.gui def test_code_input_none(qtbot): w = CodeInputWidget() - qtbot.addWidget(w) + qtbot.add_widget(w) w.set_choices(choices=["A", "B", "C", "D"], right_choice="C") with qtbot.wait_signal(w.none_clicked): qtbot.mouseClick(w.button_none, QtCore.Qt.LeftButton) diff --git a/tests/core/gui/test_create_organization.py b/tests/core/gui/test_create_organization.py index 054ae048aee..7b1b5c5a379 100644 --- a/tests/core/gui/test_create_organization.py +++ b/tests/core/gui/test_create_organization.py @@ -40,19 +40,19 @@ def _user_widget_ready(): await aqtbot.wait_until(_user_widget_ready) # Adding a few spaces to the name - await aqtbot.key_clicks(co_w.user_widget.line_edit_user_full_name, " Gordon Freeman ") - await aqtbot.key_clicks(co_w.user_widget.line_edit_user_email, "gordon.freeman@blackmesa.com") - await aqtbot.key_clicks(co_w.user_widget.line_edit_org_name, "AnomalousMaterials") + aqtbot.key_clicks(co_w.user_widget.line_edit_user_full_name, " Gordon Freeman ") + aqtbot.key_clicks(co_w.user_widget.line_edit_user_email, "gordon.freeman@blackmesa.com") + aqtbot.key_clicks(co_w.user_widget.line_edit_org_name, "AnomalousMaterials") assert not co_w.button_validate.isEnabled() - await aqtbot.mouse_click(co_w.user_widget.check_accept_contract, QtCore.Qt.LeftButton) + aqtbot.mouse_click(co_w.user_widget.check_accept_contract, QtCore.Qt.LeftButton) def _user_widget_button_validate_ready(): assert co_w.button_validate.isEnabled() await aqtbot.wait_until(_user_widget_button_validate_ready) - await aqtbot.mouse_click(co_w.button_validate, QtCore.Qt.LeftButton) + aqtbot.mouse_click(co_w.button_validate, QtCore.Qt.LeftButton) def _device_widget_ready(): assert not co_w.user_widget.isVisible() @@ -62,22 +62,20 @@ def _device_widget_ready(): await aqtbot.wait_until(_device_widget_ready) - await aqtbot.key_clicks(co_w.device_widget.line_edit_device, "HEV") + aqtbot.key_clicks(co_w.device_widget.line_edit_device, "HEV") assert co_w.device_widget.widget_password.label_password_warning.text() == translate( "TEXT_PASSWORD_WARNING" ) - await aqtbot.key_clicks(co_w.device_widget.widget_password.line_edit_password, "nihilanth") + aqtbot.key_clicks(co_w.device_widget.widget_password.line_edit_password, "nihilanth") assert not co_w.button_validate.isEnabled() - await aqtbot.key_clicks( - co_w.device_widget.widget_password.line_edit_password_check, "nihilanth" - ) + aqtbot.key_clicks(co_w.device_widget.widget_password.line_edit_password_check, "nihilanth") def _device_widget_button_validate_ready(): assert co_w.button_validate.isEnabled() await aqtbot.wait_until(_device_widget_button_validate_ready) - await aqtbot.mouse_click(co_w.button_validate, QtCore.Qt.LeftButton) + aqtbot.mouse_click(co_w.button_validate, QtCore.Qt.LeftButton) @pytest.mark.gui @@ -89,7 +87,7 @@ async def test_create_organization( # The org creation window is usually opened using a sub-menu. # Sub-menus can be a bit challenging to open in tests so we cheat # using the keyboard shortcut Ctrl+N that has the same effect. - await aqtbot.key_click(gui, "n", QtCore.Qt.ControlModifier, 200) + aqtbot.key_click(gui, "n", QtCore.Qt.ControlModifier, 200) co_w = await catch_create_org_widget() @@ -124,7 +122,7 @@ async def test_create_organization_offline( gui, aqtbot, running_backend, catch_create_org_widget, autoclose_dialog ): with running_backend.offline(): - await aqtbot.key_click(gui, "n", QtCore.Qt.ControlModifier, 200) + aqtbot.key_click(gui, "n", QtCore.Qt.ControlModifier, 200) co_w = await catch_create_org_widget() assert co_w @@ -155,7 +153,7 @@ async def test_create_organization_same_name( await bootstrap_organization(cmds, human_handle=human_handle, device_label="PC1") # Now create an org with the same name - await aqtbot.key_click(gui, "n", QtCore.Qt.ControlModifier, 200) + aqtbot.key_click(gui, "n", QtCore.Qt.ControlModifier, 200) co_w = await catch_create_org_widget() assert co_w @@ -174,28 +172,26 @@ def _modal_shown(): async def test_create_organization_previous_clicked( gui, aqtbot, running_backend, catch_create_org_widget, autoclose_dialog ): - await aqtbot.key_click(gui, "n", QtCore.Qt.ControlModifier, 200) + aqtbot.key_click(gui, "n", QtCore.Qt.ControlModifier, 200) co_w = await catch_create_org_widget() assert co_w await aqtbot.wait_until(co_w.user_widget.isVisible) - await aqtbot.key_clicks(co_w.user_widget.line_edit_user_full_name, "Gordon Freeman") - await aqtbot.key_clicks(co_w.user_widget.line_edit_user_email, "gordon.freeman@blackmesa.com") - await aqtbot.key_clicks(co_w.user_widget.line_edit_org_name, "AnomalousMaterials") - await aqtbot.mouse_click(co_w.user_widget.check_accept_contract, QtCore.Qt.LeftButton) - await aqtbot.mouse_click(co_w.button_validate, QtCore.Qt.LeftButton) + aqtbot.key_clicks(co_w.user_widget.line_edit_user_full_name, "Gordon Freeman") + aqtbot.key_clicks(co_w.user_widget.line_edit_user_email, "gordon.freeman@blackmesa.com") + aqtbot.key_clicks(co_w.user_widget.line_edit_org_name, "AnomalousMaterials") + aqtbot.mouse_click(co_w.user_widget.check_accept_contract, QtCore.Qt.LeftButton) + aqtbot.mouse_click(co_w.button_validate, QtCore.Qt.LeftButton) await aqtbot.wait_until(co_w.device_widget.isVisible) - await aqtbot.key_clicks(co_w.device_widget.line_edit_device, "HEV") - await aqtbot.key_clicks(co_w.device_widget.widget_password.line_edit_password, "nihilanth") - await aqtbot.key_clicks( - co_w.device_widget.widget_password.line_edit_password_check, "nihilanth" - ) + aqtbot.key_clicks(co_w.device_widget.line_edit_device, "HEV") + aqtbot.key_clicks(co_w.device_widget.widget_password.line_edit_password, "nihilanth") + aqtbot.key_clicks(co_w.device_widget.widget_password.line_edit_password_check, "nihilanth") - await aqtbot.mouse_click(co_w.button_previous, QtCore.Qt.LeftButton) + aqtbot.mouse_click(co_w.button_previous, QtCore.Qt.LeftButton) def _previous_page_ready(): assert co_w.user_widget.isVisible() @@ -212,7 +208,7 @@ def _previous_page_ready(): await aqtbot.wait_until(_previous_page_ready) - await aqtbot.mouse_click(co_w.button_validate, QtCore.Qt.LeftButton) + aqtbot.mouse_click(co_w.button_validate, QtCore.Qt.LeftButton) def _next_page_ready(): assert co_w.device_widget.isVisible() @@ -246,8 +242,8 @@ async def test_create_organization_bootstrap_only( "TEXT_BOOTSTRAP_ORGANIZATION_INSTRUCTIONS_organization" ).format(organization="AnomalousMaterials") - await aqtbot.key_clicks(co_w.user_widget.line_edit_user_full_name, "Gordon Freeman") - await aqtbot.key_clicks(co_w.user_widget.line_edit_user_email, "gordon.freeman@blackmesa.com") + aqtbot.key_clicks(co_w.user_widget.line_edit_user_full_name, "Gordon Freeman") + aqtbot.key_clicks(co_w.user_widget.line_edit_user_email, "gordon.freeman@blackmesa.com") def _user_widget_button_validate_ready(): assert co_w.button_validate.isEnabled() @@ -257,11 +253,11 @@ def _user_widget_button_validate_ready(): assert not co_w.user_widget.radio_use_custom.isChecked() assert co_w.user_widget.radio_use_commercial.isChecked() assert not co_w.user_widget.radio_use_custom.isEnabled() - await aqtbot.mouse_click(co_w.user_widget.check_accept_contract, QtCore.Qt.LeftButton) + aqtbot.mouse_click(co_w.user_widget.check_accept_contract, QtCore.Qt.LeftButton) await aqtbot.wait_until(_user_widget_button_validate_ready) - await aqtbot.mouse_click(co_w.button_validate, QtCore.Qt.LeftButton) + aqtbot.mouse_click(co_w.button_validate, QtCore.Qt.LeftButton) def _device_widget_ready(): assert not co_w.user_widget.isVisible() @@ -271,13 +267,11 @@ def _device_widget_ready(): await aqtbot.wait_until(_device_widget_ready) - await aqtbot.key_clicks(co_w.device_widget.line_edit_device, "HEV") - await aqtbot.key_clicks(co_w.device_widget.widget_password.line_edit_password, "nihilanth") - await aqtbot.key_clicks( - co_w.device_widget.widget_password.line_edit_password_check, "nihilanth" - ) + aqtbot.key_clicks(co_w.device_widget.line_edit_device, "HEV") + aqtbot.key_clicks(co_w.device_widget.widget_password.line_edit_password, "nihilanth") + aqtbot.key_clicks(co_w.device_widget.widget_password.line_edit_password_check, "nihilanth") - await aqtbot.mouse_click(co_w.button_validate, QtCore.Qt.LeftButton) + aqtbot.mouse_click(co_w.button_validate, QtCore.Qt.LeftButton) def _modal_shown(): assert autoclose_dialog.dialogs == [ @@ -317,8 +311,8 @@ async def test_create_organization_bootstrap_only_custom_server( "TEXT_BOOTSTRAP_ORGANIZATION_INSTRUCTIONS_organization" ).format(organization="AnomalousMaterials") - await aqtbot.key_clicks(co_w.user_widget.line_edit_user_full_name, "Gordon Freeman") - await aqtbot.key_clicks(co_w.user_widget.line_edit_user_email, "gordon.freeman@blackmesa.com") + aqtbot.key_clicks(co_w.user_widget.line_edit_user_full_name, "Gordon Freeman") + aqtbot.key_clicks(co_w.user_widget.line_edit_user_email, "gordon.freeman@blackmesa.com") def _user_widget_ready(): assert co_w.user_widget.line_edit_org_name.text() == "AnomalousMaterials" @@ -331,9 +325,9 @@ def _user_widget_ready(): await aqtbot.wait_until(_user_widget_ready) - await aqtbot.mouse_click(co_w.user_widget.check_accept_contract, QtCore.Qt.LeftButton) + aqtbot.mouse_click(co_w.user_widget.check_accept_contract, QtCore.Qt.LeftButton) - await aqtbot.mouse_click(co_w.button_validate, QtCore.Qt.LeftButton) + aqtbot.mouse_click(co_w.button_validate, QtCore.Qt.LeftButton) def _device_widget_ready(): assert not co_w.user_widget.isVisible() @@ -343,13 +337,11 @@ def _device_widget_ready(): await aqtbot.wait_until(_device_widget_ready) - await aqtbot.key_clicks(co_w.device_widget.line_edit_device, "HEV") - await aqtbot.key_clicks(co_w.device_widget.widget_password.line_edit_password, "nihilanth") - await aqtbot.key_clicks( - co_w.device_widget.widget_password.line_edit_password_check, "nihilanth" - ) + aqtbot.key_clicks(co_w.device_widget.line_edit_device, "HEV") + aqtbot.key_clicks(co_w.device_widget.widget_password.line_edit_password, "nihilanth") + aqtbot.key_clicks(co_w.device_widget.widget_password.line_edit_password_check, "nihilanth") - await aqtbot.mouse_click(co_w.button_validate, QtCore.Qt.LeftButton) + aqtbot.mouse_click(co_w.button_validate, QtCore.Qt.LeftButton) def _modal_shown(): assert autoclose_dialog.dialogs == [ @@ -402,13 +394,13 @@ async def test_create_organization_already_bootstrapped( # The org bootstrap window is usually opened using a sub-menu. # Sub-menus can be a bit challenging to open in tests so we cheat # using the keyboard shortcut Ctrl+O that has the same effect. - await aqtbot.key_click(gui, "o", QtCore.Qt.ControlModifier, 200) + aqtbot.key_click(gui, "o", QtCore.Qt.ControlModifier, 200) co_w = await catch_create_org_widget() await aqtbot.wait_until(co_w.user_widget.isVisible) - await aqtbot.key_clicks(co_w.user_widget.line_edit_user_full_name, "Gordon Freeman") - await aqtbot.key_clicks(co_w.user_widget.line_edit_user_email, "gordon.freeman@blackmesa.com") + aqtbot.key_clicks(co_w.user_widget.line_edit_user_full_name, "Gordon Freeman") + aqtbot.key_clicks(co_w.user_widget.line_edit_user_email, "gordon.freeman@blackmesa.com") def _user_widget_ready(): assert co_w.user_widget.line_edit_org_name.text() == org.organization_id @@ -416,17 +408,15 @@ def _user_widget_ready(): await aqtbot.wait_until(_user_widget_ready) - await aqtbot.mouse_click(co_w.user_widget.check_accept_contract, QtCore.Qt.LeftButton) - await aqtbot.mouse_click(co_w.button_validate, QtCore.Qt.LeftButton) + aqtbot.mouse_click(co_w.user_widget.check_accept_contract, QtCore.Qt.LeftButton) + aqtbot.mouse_click(co_w.button_validate, QtCore.Qt.LeftButton) await aqtbot.wait_until(co_w.device_widget.isVisible) - await aqtbot.key_clicks(co_w.device_widget.line_edit_device, "HEV") - await aqtbot.key_clicks(co_w.device_widget.widget_password.line_edit_password, "nihilanth") - await aqtbot.key_clicks( - co_w.device_widget.widget_password.line_edit_password_check, "nihilanth" - ) - await aqtbot.mouse_click(co_w.button_validate, QtCore.Qt.LeftButton) + aqtbot.key_clicks(co_w.device_widget.line_edit_device, "HEV") + aqtbot.key_clicks(co_w.device_widget.widget_password.line_edit_password, "nihilanth") + aqtbot.key_clicks(co_w.device_widget.widget_password.line_edit_password_check, "nihilanth") + aqtbot.mouse_click(co_w.button_validate, QtCore.Qt.LeftButton) def _modal_shown(): assert autoclose_dialog.dialogs == [("Error", "This bootstrap link was already used.")] @@ -444,7 +434,7 @@ async def test_create_organization_custom_backend( # The org creation window is usually opened using a sub-menu. # Sub-menus can be a bit challenging to open in tests so we cheat # using the keyboard shortcut Ctrl+N that has the same effect. - await aqtbot.key_click(gui, "n", QtCore.Qt.ControlModifier, 200) + aqtbot.key_click(gui, "n", QtCore.Qt.ControlModifier, 200) co_w = await catch_create_org_widget() @@ -458,16 +448,16 @@ def _user_widget_ready(): await aqtbot.wait_until(_user_widget_ready) - await aqtbot.key_clicks(co_w.user_widget.line_edit_user_full_name, "Gordon Freeman") - await aqtbot.key_clicks(co_w.user_widget.line_edit_user_email, "gordon.freeman@blackmesa.com") - await aqtbot.key_clicks(co_w.user_widget.line_edit_org_name, "AnomalousMaterials") - await aqtbot.mouse_click(co_w.user_widget.check_accept_contract, QtCore.Qt.LeftButton) + aqtbot.key_clicks(co_w.user_widget.line_edit_user_full_name, "Gordon Freeman") + aqtbot.key_clicks(co_w.user_widget.line_edit_user_email, "gordon.freeman@blackmesa.com") + aqtbot.key_clicks(co_w.user_widget.line_edit_org_name, "AnomalousMaterials") + aqtbot.mouse_click(co_w.user_widget.check_accept_contract, QtCore.Qt.LeftButton) def _user_widget_button_validate_ready(): assert co_w.button_validate.isEnabled() await aqtbot.wait_until(_user_widget_button_validate_ready) - await aqtbot.mouse_click(co_w.button_validate, QtCore.Qt.LeftButton) + aqtbot.mouse_click(co_w.button_validate, QtCore.Qt.LeftButton) def _device_widget_ready(): assert not co_w.user_widget.isVisible() @@ -477,18 +467,16 @@ def _device_widget_ready(): await aqtbot.wait_until(_device_widget_ready) - await aqtbot.key_clicks(co_w.device_widget.line_edit_device, "HEV") - await aqtbot.key_clicks(co_w.device_widget.widget_password.line_edit_password, "nihilanth") - await aqtbot.key_clicks( - co_w.device_widget.widget_password.line_edit_password_check, "nihilanth" - ) + aqtbot.key_clicks(co_w.device_widget.line_edit_device, "HEV") + aqtbot.key_clicks(co_w.device_widget.widget_password.line_edit_password, "nihilanth") + aqtbot.key_clicks(co_w.device_widget.widget_password.line_edit_password_check, "nihilanth") def _device_widget_button_validate_ready(): assert co_w.button_validate.isEnabled() await aqtbot.wait_until(_device_widget_button_validate_ready) - await aqtbot.mouse_click(co_w.button_validate, QtCore.Qt.LeftButton) + aqtbot.mouse_click(co_w.button_validate, QtCore.Qt.LeftButton) # Should fail because it will use an invalid backend addr def _error_modal_shown(): @@ -499,7 +487,7 @@ def _error_modal_shown(): autoclose_dialog.reset() # Let's go back and provide a custom address - await aqtbot.mouse_click(co_w.button_previous, QtCore.Qt.LeftButton) + aqtbot.mouse_click(co_w.button_previous, QtCore.Qt.LeftButton) def _user_widget_ready_again(): assert co_w.user_widget.isVisible() @@ -512,13 +500,13 @@ def _user_widget_ready_again(): # Clicking the radio doesn't do anything, so we cheat co_w.user_widget.radio_use_custom.setChecked(True) - await aqtbot.key_clicks(co_w.user_widget.line_edit_backend_addr, running_backend.addr.to_url()) + aqtbot.key_clicks(co_w.user_widget.line_edit_backend_addr, running_backend.addr.to_url()) await aqtbot.wait_until(_user_widget_button_validate_ready) # First click to get to the device page - await aqtbot.mouse_click(co_w.button_validate, QtCore.Qt.LeftButton) + aqtbot.mouse_click(co_w.button_validate, QtCore.Qt.LeftButton) # Second click to create the org - await aqtbot.mouse_click(co_w.button_validate, QtCore.Qt.LeftButton) + aqtbot.mouse_click(co_w.button_validate, QtCore.Qt.LeftButton) def _modal_shown(): assert autoclose_dialog.dialogs == [ @@ -540,7 +528,7 @@ def _modal_shown(): async def test_create_organization_wrong_timestamp( gui, aqtbot, running_backend, catch_create_org_widget, autoclose_dialog, monkeypatch ): - await aqtbot.key_click(gui, "n", QtCore.Qt.ControlModifier, 200) + aqtbot.key_click(gui, "n", QtCore.Qt.ControlModifier, 200) co_w = await catch_create_org_widget() assert co_w @@ -602,17 +590,15 @@ def _user_widget_ready(): await aqtbot.wait_until(_user_widget_ready) - await aqtbot.key_clicks(co_w.user_widget.line_edit_user_full_name, "Gordon Freeman") - await aqtbot.key_clicks( - co_w.user_widget.line_edit_user_email, "gordon.freeman@blackmesa.com" - ) - await aqtbot.mouse_click(co_w.user_widget.check_accept_contract, QtCore.Qt.LeftButton) + aqtbot.key_clicks(co_w.user_widget.line_edit_user_full_name, "Gordon Freeman") + aqtbot.key_clicks(co_w.user_widget.line_edit_user_email, "gordon.freeman@blackmesa.com") + aqtbot.mouse_click(co_w.user_widget.check_accept_contract, QtCore.Qt.LeftButton) def _user_widget_button_validate_ready(): assert co_w.button_validate.isEnabled() await aqtbot.wait_until(_user_widget_button_validate_ready) - await aqtbot.mouse_click(co_w.button_validate, QtCore.Qt.LeftButton) + aqtbot.mouse_click(co_w.button_validate, QtCore.Qt.LeftButton) def _device_widget_ready(): assert not co_w.user_widget.isVisible() @@ -622,18 +608,16 @@ def _device_widget_ready(): await aqtbot.wait_until(_device_widget_ready) - await aqtbot.key_clicks(co_w.device_widget.line_edit_device, "HEV") - await aqtbot.key_clicks(co_w.device_widget.widget_password.line_edit_password, "nihilanth") - await aqtbot.key_clicks( - co_w.device_widget.widget_password.line_edit_password_check, "nihilanth" - ) + aqtbot.key_clicks(co_w.device_widget.line_edit_device, "HEV") + aqtbot.key_clicks(co_w.device_widget.widget_password.line_edit_password, "nihilanth") + aqtbot.key_clicks(co_w.device_widget.widget_password.line_edit_password_check, "nihilanth") def _device_widget_button_validate_ready(): assert co_w.button_validate.isEnabled() await aqtbot.wait_until(_device_widget_button_validate_ready) - await aqtbot.mouse_click(co_w.button_validate, QtCore.Qt.LeftButton) + aqtbot.mouse_click(co_w.button_validate, QtCore.Qt.LeftButton) if bootstrap_addr is bad_bootstrap_addr: diff --git a/tests/core/gui/test_custom_widgets.py b/tests/core/gui/test_custom_widgets.py index 7e6cedd6c40..d519f3a5edb 100644 --- a/tests/core/gui/test_custom_widgets.py +++ b/tests/core/gui/test_custom_widgets.py @@ -9,7 +9,7 @@ def test_file_line_edit(qtbot): w = custom_widgets.FileLabel(parent=None) - qtbot.addWidget(w) + qtbot.add_widget(w) w.setText("A_short_file_name.txt") assert w.text() == "A_short_file_name.txt" diff --git a/tests/core/gui/test_expired_organization.py b/tests/core/gui/test_expired_organization.py index 65810709c3f..069d32fc871 100644 --- a/tests/core/gui/test_expired_organization.py +++ b/tests/core/gui/test_expired_organization.py @@ -34,10 +34,10 @@ def _password_widget_shown(): password_w = lw.widget.layout().itemAt(0).widget() - await aqtbot.key_clicks(password_w.line_edit_password, "P@ssw0rd") + aqtbot.key_clicks(password_w.line_edit_password, "P@ssw0rd") async with aqtbot.wait_signals([lw.login_with_password_clicked, tabw.logged_in]): - await aqtbot.mouse_click(password_w.button_login, QtCore.Qt.LeftButton) + aqtbot.mouse_click(password_w.button_login, QtCore.Qt.LeftButton) # Assert dialog def _expired_notified(): @@ -66,10 +66,10 @@ def _password_widget_shown(): password_w = lw.widget.layout().itemAt(0).widget() - await aqtbot.key_clicks(password_w.line_edit_password, "P@ssw0rd") + aqtbot.key_clicks(password_w.line_edit_password, "P@ssw0rd") async with aqtbot.wait_signals([lw.login_with_password_clicked, tabw.logged_in]): - await aqtbot.mouse_click(password_w.button_login, QtCore.Qt.LeftButton) + aqtbot.mouse_click(password_w.button_login, QtCore.Qt.LeftButton) # Assert logged in def _notified(): diff --git a/tests/core/gui/test_file_table.py b/tests/core/gui/test_file_table.py index 8615d629c60..8c1a992be94 100644 --- a/tests/core/gui/test_file_table.py +++ b/tests/core/gui/test_file_table.py @@ -15,7 +15,7 @@ def test_file_table_parent_folder(qtbot, core_config): switch_language(core_config, "en") w = FileTable(parent=None) - qtbot.addWidget(w) + qtbot.add_widget(w) assert w.rowCount() == 0 assert w.columnCount() == 5 @@ -30,7 +30,7 @@ def test_file_table_parent_workspace(qtbot, core_config): switch_language(core_config, "en") w = FileTable(parent=None) - qtbot.addWidget(w) + qtbot.add_widget(w) assert w.rowCount() == 0 assert w.columnCount() == 5 @@ -44,7 +44,7 @@ def test_file_table_parent_workspace(qtbot, core_config): def test_file_table_clear(qtbot): w = FileTable(parent=None) - qtbot.addWidget(w) + qtbot.add_widget(w) w.add_parent_workspace() assert w.rowCount() == 1 @@ -57,7 +57,7 @@ def test_file_table_sort(qtbot, core_config): switch_language(core_config, "en") w = FileTable(parent=None) - qtbot.addWidget(w) + qtbot.add_widget(w) w.add_parent_workspace() w.add_folder("Dir1", uuid.uuid4(), True, False) w.add_file( diff --git a/tests/core/gui/test_files.py b/tests/core/gui/test_files.py index 0ec967a3f39..e6326a652cd 100644 --- a/tests/core/gui/test_files.py +++ b/tests/core/gui/test_files.py @@ -113,19 +113,19 @@ async def copy(self, selection=None): if selection is not None: await self.apply_selection(selection) async with aqtbot.wait_signal(f_w.table_files.copy_clicked): - await aqtbot.key_click(f_w.table_files, "C", modifier=QtCore.Qt.ControlModifier) + aqtbot.key_click(f_w.table_files, "C", modifier=QtCore.Qt.ControlModifier) assert f_w.clipboard is not None async def cut(self, selection=None): if selection is not None: await self.apply_selection(selection) async with aqtbot.wait_signal(f_w.table_files.cut_clicked): - await aqtbot.key_click(f_w.table_files, "X", modifier=QtCore.Qt.ControlModifier) + aqtbot.key_click(f_w.table_files, "X", modifier=QtCore.Qt.ControlModifier) assert f_w.clipboard is not None async def paste(self): async with aqtbot.wait_signal(f_w.table_files.paste_clicked): - await aqtbot.key_click(f_w.table_files, "V", modifier=QtCore.Qt.ControlModifier) + aqtbot.key_click(f_w.table_files, "V", modifier=QtCore.Qt.ControlModifier) async def check_files_view(self, path, expected_entries, workspace_name="wksp1"): expected_table_files = [] @@ -161,7 +161,7 @@ async def create_folder(self, name, wait_until=None): monkeypatch.setattr( "parsec.core.gui.files_widget.get_text_input", lambda *args, **kwargs: (name) ) - await aqtbot.mouse_click(f_w.button_create_folder, QtCore.Qt.LeftButton) + aqtbot.mouse_click(f_w.button_create_folder, QtCore.Qt.LeftButton) def _folder_created(): for i in range(f_w.table_files.rowCount()): @@ -248,7 +248,7 @@ async def test_file_browsing_and_edit( ), ) async with aqtbot.wait_signal(f_w.import_success): - await aqtbot.mouse_click(f_w.button_import_files, QtCore.Qt.LeftButton) + aqtbot.mouse_click(f_w.button_import_files, QtCore.Qt.LeftButton) await tb.check_files_view( path="/", expected_entries=["dir0/", "dir1/", "zdir2/", "file1.txt", "file2.txt"] ) @@ -259,7 +259,7 @@ async def test_file_browsing_and_edit( classmethod(lambda *args, **kwargs: out_of_parsec_data / "dir3"), ) async with aqtbot.wait_signal(f_w.import_success): - await aqtbot.mouse_click(f_w.button_import_folder, QtCore.Qt.LeftButton) + aqtbot.mouse_click(f_w.button_import_folder, QtCore.Qt.LeftButton) await tb.check_files_view( path="/", expected_entries=["dir0/", "dir1/", "dir3/", "zdir2/", "file1.txt", "file2.txt"] ) @@ -573,7 +573,7 @@ async def test_import_file_permission_denied( ) async with aqtbot.wait_signal(f_w.button_import_files.clicked): - await aqtbot.mouse_click(f_w.button_import_files, QtCore.Qt.LeftButton) + aqtbot.mouse_click(f_w.button_import_files, QtCore.Qt.LeftButton) def _import_failed(): assert autoclose_dialog.dialogs == [ @@ -593,7 +593,7 @@ def _import_failed(): ) async with aqtbot.wait_signal(f_w.button_import_files.clicked): - await aqtbot.mouse_click(f_w.button_import_files, QtCore.Qt.LeftButton) + aqtbot.mouse_click(f_w.button_import_files, QtCore.Qt.LeftButton) def _import_error_shown(): assert autoclose_dialog.dialogs == [ diff --git a/tests/core/gui/test_input_dialogs.py b/tests/core/gui/test_input_dialogs.py index b1aa00f47e9..f13e4bdb0d4 100644 --- a/tests/core/gui/test_input_dialogs.py +++ b/tests/core/gui/test_input_dialogs.py @@ -10,7 +10,7 @@ def test_get_text_dialog_close(qtbot): w = custom_dialogs.TextInputWidget(message="Message") d = custom_dialogs.GreyedDialog(w, title="Title", parent=None) - qtbot.addWidget(d) + qtbot.add_widget(d) d.show() assert d.isVisible() is True @@ -32,7 +32,7 @@ def test_get_text_dialog_accept(qtbot): ) d = custom_dialogs.GreyedDialog(w, title="Title", parent=None) w.dialog = d - qtbot.addWidget(d) + qtbot.add_widget(d) d.show() assert d.isVisible() is True @@ -54,7 +54,7 @@ def test_ask_question_no(qtbot): w = custom_dialogs.QuestionWidget(message="Message", button_texts=["YES", "NO"]) d = custom_dialogs.GreyedDialog(w, title="Title", parent=None) w.dialog = d - qtbot.addWidget(d) + qtbot.add_widget(d) d.show() assert d.isVisible() is True @@ -74,7 +74,7 @@ def test_ask_question_yes(qtbot): w = custom_dialogs.QuestionWidget(message="Message", button_texts=["YES", "NO"]) d = custom_dialogs.GreyedDialog(w, title="Title", parent=None) w.dialog = d - qtbot.addWidget(d) + qtbot.add_widget(d) d.show() assert d.isVisible() is True @@ -94,7 +94,7 @@ def test_ask_question_close(qtbot): w = custom_dialogs.QuestionWidget(message="Message", button_texts=["YES", "NO"]) d = custom_dialogs.GreyedDialog(w, title="Title", parent=None) w.dialog = d - qtbot.addWidget(d) + qtbot.add_widget(d) d.show() assert d.isVisible() is True diff --git a/tests/core/gui/test_keys.py b/tests/core/gui/test_keys.py index 2a750f29152..40fa967b6a0 100644 --- a/tests/core/gui/test_keys.py +++ b/tests/core/gui/test_keys.py @@ -16,7 +16,7 @@ def keys_widget(qtbot, core_config, alice, bob): save_device_with_password(core_config.config_dir, bob, password) w = KeysWidget(core_config, parent=None) - qtbot.addWidget(w) + qtbot.add_widget(w) assert w.scroll_content.layout().count() == 2 return w @@ -67,7 +67,7 @@ def test_keys_import(qtbot, core_config, alice, bob, monkeypatch): fake_config = type("fake_config", (), {"config_dir": tmp_path})() w = KeysWidget(fake_config, parent=None) - qtbot.addWidget(w) + qtbot.add_widget(w) keys_layout = w.scroll_content.layout() assert keys_layout.count() == 0 diff --git a/tests/core/gui/test_loading_dialog.py b/tests/core/gui/test_loading_dialog.py index b55ab4848b5..707e799eb39 100644 --- a/tests/core/gui/test_loading_dialog.py +++ b/tests/core/gui/test_loading_dialog.py @@ -10,7 +10,7 @@ def test_loading_dialog(qtbot): w = LoadingWidget(total_size=10000) - qtbot.addWidget(w) + qtbot.add_widget(w) assert w.progress_bar.text() == "0%" assert w.progress_bar.value() == 0 @@ -36,7 +36,7 @@ def test_loading_dialog_overflow(qtbot): # Test for a file length > to 2Go (overflow of signed int32) w = LoadingWidget(total_size=3000000000) - qtbot.addWidget(w) + qtbot.add_widget(w) assert w.progress_bar.text() == "0%" assert w.progress_bar.value() == 0 diff --git a/tests/core/gui/test_login.py b/tests/core/gui/test_login.py index 668474ace7b..decbd0c0c21 100644 --- a/tests/core/gui/test_login.py +++ b/tests/core/gui/test_login.py @@ -43,10 +43,10 @@ def _password_widget_shown(): password_w = lw.widget.layout().itemAt(0).widget() - await aqtbot.key_clicks(password_w.line_edit_password, "P@ssw0rd") + aqtbot.key_clicks(password_w.line_edit_password, "P@ssw0rd") async with aqtbot.wait_signals([lw.login_with_password_clicked, tabw.logged_in]): - await aqtbot.mouse_click(password_w.button_login, QtCore.Qt.LeftButton) + aqtbot.mouse_click(password_w.button_login, QtCore.Qt.LeftButton) central_widget = gui.test_get_central_widget() assert central_widget is not None @@ -73,7 +73,7 @@ async def test_login_back_to_account_list( assert accounts_w async with aqtbot.wait_signal(accounts_w.account_clicked): - await aqtbot.mouse_click( + aqtbot.mouse_click( accounts_w.accounts_widget.layout().itemAt(0).widget(), QtCore.Qt.LeftButton ) @@ -85,7 +85,7 @@ def _password_widget_shown(): password_w = lw.widget.layout().itemAt(0).widget() async with aqtbot.wait_signal(password_w.back_clicked): - await aqtbot.mouse_click(password_w.button_back, QtCore.Qt.LeftButton) + aqtbot.mouse_click(password_w.button_back, QtCore.Qt.LeftButton) def _account_widget_shown(): assert isinstance(lw.widget.layout().itemAt(0).widget(), LoginAccountsWidget) @@ -176,9 +176,7 @@ async def test_login_logout_account_list_refresh( assert acc_w.accounts_widget.layout().count() == 3 async with aqtbot.wait_signal(acc_w.account_clicked): - await aqtbot.mouse_click( - acc_w.accounts_widget.layout().itemAt(0).widget(), QtCore.Qt.LeftButton - ) + aqtbot.mouse_click(acc_w.accounts_widget.layout().itemAt(0).widget(), QtCore.Qt.LeftButton) def _password_widget_shown(): assert isinstance(lw.widget.layout().itemAt(0).widget(), LoginPasswordInputWidget) @@ -187,10 +185,10 @@ def _password_widget_shown(): password_w = lw.widget.layout().itemAt(0).widget() - await aqtbot.key_clicks(password_w.line_edit_password, password) + aqtbot.key_clicks(password_w.line_edit_password, password) async with aqtbot.wait_signals([lw.login_with_password_clicked, tabw.logged_in]): - await aqtbot.mouse_click(password_w.button_login, QtCore.Qt.LeftButton) + aqtbot.mouse_click(password_w.button_login, QtCore.Qt.LeftButton) central_widget = gui.test_get_central_widget() assert central_widget is not None @@ -199,7 +197,7 @@ def _password_widget_shown(): assert corner_widget.isVisible() # Now add a new tab - await aqtbot.mouse_click(corner_widget, QtCore.Qt.LeftButton) + aqtbot.mouse_click(corner_widget, QtCore.Qt.LeftButton) def _switch_to_login_tab(): assert gui.tab_center.count() == 2 diff --git a/tests/core/gui/test_main_window.py b/tests/core/gui/test_main_window.py index 209c678d33e..f62b2a74c10 100644 --- a/tests/core/gui/test_main_window.py +++ b/tests/core/gui/test_main_window.py @@ -104,7 +104,7 @@ async def logged_gui_with_files( "parsec.core.gui.files_widget.get_text_input", lambda *args, **kwargs: ("dir1") ) - await aqtbot.mouse_click(w_w.button_add_workspace, QtCore.Qt.LeftButton) + aqtbot.mouse_click(w_w.button_add_workspace, QtCore.Qt.LeftButton) def _workspace_button_ready(): assert w_w.layout_workspaces.count() == 1 @@ -118,7 +118,7 @@ def _workspace_button_ready(): f_w = logged_gui.test_get_files_widget() wk_button = w_w.layout_workspaces.itemAt(0).widget() - await aqtbot.mouse_click(wk_button, QtCore.Qt.LeftButton) + aqtbot.mouse_click(wk_button, QtCore.Qt.LeftButton) def _entry_available(): assert f_w.workspace_fs is not None @@ -134,7 +134,7 @@ def _folder_ready(): assert folder assert folder.text() == "dir1" - await aqtbot.mouse_click(f_w.button_create_folder, QtCore.Qt.LeftButton) + aqtbot.mouse_click(f_w.button_create_folder, QtCore.Qt.LeftButton) await aqtbot.wait_until(_folder_ready) @@ -287,7 +287,7 @@ def _password_widget_shown(): password_w = lw.widget.layout().itemAt(0).widget() # Connect to the organization - await aqtbot.mouse_click(password_w.button_login, QtCore.Qt.LeftButton) + aqtbot.mouse_click(password_w.button_login, QtCore.Qt.LeftButton) def _wait(): central_widget = gui.test_get_central_widget() @@ -352,7 +352,7 @@ def _password_widget_shown(): # Cancel login async with aqtbot.wait_signal(lw.login_canceled): - await aqtbot.mouse_click(password_w.button_back, QtCore.Qt.LeftButton) + aqtbot.mouse_click(password_w.button_back, QtCore.Qt.LeftButton) await gui.test_switch_to_logged_in(bob) @@ -497,7 +497,7 @@ async def test_tab_login_logout_two_tabs(aqtbot, gui_factory, core_config, alice assert gui.tab_center.tabText(0) == "CoolOrg - Alicey..." logged_tab = gui.test_get_tab() - await aqtbot.mouse_click(gui.add_tab_button, QtCore.Qt.LeftButton) + aqtbot.mouse_click(gui.add_tab_button, QtCore.Qt.LeftButton) assert gui.tab_center.count() == 2 assert gui.tab_center.tabText(0) == "CoolOrg - Alicey..." assert gui.tab_center.tabText(1) == translate("TEXT_TAB_TITLE_LOG_IN_SCREEN") @@ -536,7 +536,7 @@ async def test_tab_login_logout_two_tabs_logged_in( assert gui.tab_center.tabText(0) == "CoolOrg - Alicey..." alice_logged_tab = gui.test_get_tab() - await aqtbot.mouse_click(gui.add_tab_button, QtCore.Qt.LeftButton) + aqtbot.mouse_click(gui.add_tab_button, QtCore.Qt.LeftButton) assert gui.tab_center.count() == 2 assert gui.tab_center.tabText(0) == "CoolOrg - Alicey..." assert gui.tab_center.tabText(1) == translate("TEXT_TAB_TITLE_LOG_IN_SCREEN") diff --git a/tests/core/gui/test_menu.py b/tests/core/gui/test_menu.py index a5cc5ee8272..4f5cae68a25 100644 --- a/tests/core/gui/test_menu.py +++ b/tests/core/gui/test_menu.py @@ -10,7 +10,7 @@ def test_activate_files(qtbot): w = MenuWidget(parent=None) - qtbot.addWidget(w) + qtbot.add_widget(w) w.button_devices.setChecked(True) w.button_users.setChecked(True) @@ -27,7 +27,7 @@ def test_activate_files(qtbot): def test_activate_users(qtbot): w = MenuWidget(parent=None) - qtbot.addWidget(w) + qtbot.add_widget(w) w.button_files.setChecked(True) w.button_devices.setChecked(True) @@ -44,7 +44,7 @@ def test_activate_users(qtbot): def test_activate_devices(qtbot): w = MenuWidget(parent=None) - qtbot.addWidget(w) + qtbot.add_widget(w) w.button_files.setChecked(True) w.button_users.setChecked(True) @@ -60,7 +60,7 @@ def test_activate_devices(qtbot): @pytest.mark.gui def test_clicked(qtbot): w = MenuWidget(parent=None) - qtbot.addWidget(w) + qtbot.add_widget(w) w.button_files.setChecked(True) w.button_users.setChecked(True) diff --git a/tests/core/gui/test_password_change.py b/tests/core/gui/test_password_change.py index e1c35780bb5..ed26ce73533 100644 --- a/tests/core/gui/test_password_change.py +++ b/tests/core/gui/test_password_change.py @@ -25,10 +25,10 @@ async def test_change_password_invalid_old_password( pc_w = await catch_password_change_widget() - await aqtbot.key_clicks(pc_w.line_edit_old_password, "0123456789") - await aqtbot.key_clicks(pc_w.widget_new_password.line_edit_password, "P@ssw0rd2") - await aqtbot.key_clicks(pc_w.widget_new_password.line_edit_password_check, "P@ssw0rd2") - await aqtbot.mouse_click(pc_w.button_change, QtCore.Qt.LeftButton) + aqtbot.key_clicks(pc_w.line_edit_old_password, "0123456789") + aqtbot.key_clicks(pc_w.widget_new_password.line_edit_password, "P@ssw0rd2") + aqtbot.key_clicks(pc_w.widget_new_password.line_edit_password_check, "P@ssw0rd2") + aqtbot.mouse_click(pc_w.button_change, QtCore.Qt.LeftButton) assert autoclose_dialog.dialogs == [ ("Error", "You did not provide the right password for this device.") @@ -49,9 +49,9 @@ async def test_change_password_invalid_password_check( pc_w = await catch_password_change_widget() - await aqtbot.key_clicks(pc_w.line_edit_old_password, "P@ssw0rd") - await aqtbot.key_clicks(pc_w.widget_new_password.line_edit_password, "P@ssw0rd2") - await aqtbot.key_clicks(pc_w.widget_new_password.line_edit_password_check, "P@ssw0rd3") + aqtbot.key_clicks(pc_w.line_edit_old_password, "P@ssw0rd") + aqtbot.key_clicks(pc_w.widget_new_password.line_edit_password, "P@ssw0rd2") + aqtbot.key_clicks(pc_w.widget_new_password.line_edit_password_check, "P@ssw0rd3") assert not pc_w.button_change.isEnabled() @@ -69,10 +69,10 @@ async def test_change_password_success( pc_w = await catch_password_change_widget() - await aqtbot.key_clicks(pc_w.line_edit_old_password, "P@ssw0rd") - await aqtbot.key_clicks(pc_w.widget_new_password.line_edit_password, "P@ssw0rd2") - await aqtbot.key_clicks(pc_w.widget_new_password.line_edit_password_check, "P@ssw0rd2") - await aqtbot.mouse_click(pc_w.button_change, QtCore.Qt.LeftButton) + aqtbot.key_clicks(pc_w.line_edit_old_password, "P@ssw0rd") + aqtbot.key_clicks(pc_w.widget_new_password.line_edit_password, "P@ssw0rd2") + aqtbot.key_clicks(pc_w.widget_new_password.line_edit_password_check, "P@ssw0rd2") + aqtbot.mouse_click(pc_w.button_change, QtCore.Qt.LeftButton) assert autoclose_dialog.dialogs == [("", "The password has been successfully changed.")] autoclose_dialog.reset() @@ -89,14 +89,14 @@ async def test_change_password_success( password_w = l_w.widget.layout().itemAt(0).widget() assert isinstance(password_w, LoginPasswordInputWidget) - await aqtbot.key_clicks(password_w.line_edit_password, "P@ssw0rd2") + aqtbot.key_clicks(password_w.line_edit_password, "P@ssw0rd2") print(password_w.line_edit_password.text()) tabw = logged_gui.test_get_tab() async with aqtbot.wait_signals([l_w.login_with_password_clicked, tabw.logged_in]): - await aqtbot.mouse_click(password_w.button_login, QtCore.Qt.LeftButton) + aqtbot.mouse_click(password_w.button_login, QtCore.Qt.LeftButton) def _wait_logged_in(): assert not l_w.isVisible() diff --git a/tests/core/gui/test_password_validation.py b/tests/core/gui/test_password_validation.py index 2c1b93becae..11cbe94ad2c 100644 --- a/tests/core/gui/test_password_validation.py +++ b/tests/core/gui/test_password_validation.py @@ -42,7 +42,7 @@ def test_password_choice_widget(qtbot, core_config): switch_language(core_config, "en") p = PasswordChoiceWidget(parent=None) - qtbot.addWidget(p) + qtbot.add_widget(p) p.line_edit_password.setText("William J Blazkowicz") p.line_edit_password_check.setText("William J Blazkowicz") @@ -57,7 +57,7 @@ def test_password_choice_widget_mismatch(qtbot, core_config): switch_language(core_config, "en") p = PasswordChoiceWidget(parent=None) - qtbot.addWidget(p) + qtbot.add_widget(p) p.line_edit_password.setText("William J Blazkowicz") p.line_edit_password_check.setText("William J Blazkowiz") @@ -76,7 +76,7 @@ def test_password_choice_widget_with_excluded_strings(qtbot, core_config): p = PasswordChoiceWidget(parent=None) p.set_excluded_strings(["william.j.blazkowicz@wolfenstein.de"]) assert p.pwd_str_widget._excluded_strings == ["william", "blazkowicz", "wolfenstein"] - qtbot.addWidget(p) + qtbot.add_widget(p) p.line_edit_password.setText("William J Blazkowicz") diff --git a/tests/core/gui/test_validators.py b/tests/core/gui/test_validators.py index b9deaf7fdbe..624f60cc125 100644 --- a/tests/core/gui/test_validators.py +++ b/tests/core/gui/test_validators.py @@ -15,7 +15,7 @@ def test_device_name_validator(qtbot, core_config): le = ValidatedLineEdit() le.set_validator(validators.DeviceNameValidator()) - qtbot.addWidget(le) + qtbot.add_widget(le) le.show() qtbot.keyClicks(le, "abcd") @@ -33,7 +33,7 @@ def test_email_validator(qtbot, core_config): le = ValidatedLineEdit() le.set_validator(validators.EmailValidator()) - qtbot.addWidget(le) + qtbot.add_widget(le) le.show() qtbot.keyClicks(le, "maurice") @@ -55,7 +55,7 @@ def test_organization_validator(qtbot, core_config): le = ValidatedLineEdit() le.set_validator(validators.OrganizationIDValidator()) - qtbot.addWidget(le) + qtbot.add_widget(le) le.show() qtbot.keyClicks(le, "Reynholm") diff --git a/tests/core/gui/test_workspace_button.py b/tests/core/gui/test_workspace_button.py index 097ce8fc156..c8a76b45ae1 100644 --- a/tests/core/gui/test_workspace_button.py +++ b/tests/core/gui/test_workspace_button.py @@ -54,7 +54,7 @@ async def test_workspace_button(qtbot, workspace_fs, core_config, alice_user_inf is_mounted=True, files=[], ) - qtbot.addWidget(w) + qtbot.add_widget(w) w.show() assert w.widget_empty.isVisible() is True @@ -86,7 +86,7 @@ async def test_workspace_button_owned_by( files=[], ) - qtbot.addWidget(w) + qtbot.add_widget(w) w.show() assert w.widget_empty.isVisible() is True assert w.widget_files.isVisible() is False @@ -117,7 +117,7 @@ async def test_workspace_button_shared_with( files=[], ) - qtbot.addWidget(w) + qtbot.add_widget(w) w.show() assert w.widget_empty.isVisible() is True assert w.widget_files.isVisible() is False @@ -143,7 +143,7 @@ async def test_workspace_button_files(qtbot, workspace_fs, core_config, alice_us files=["File1.txt", "File2.txt", "Dir1"], ) - qtbot.addWidget(w) + qtbot.add_widget(w) w.show() assert w.widget_empty.isVisible() is False assert w.widget_files.isVisible() is True @@ -170,7 +170,7 @@ async def test_workspace_button_clicked(qtbot, workspace_fs, core_config, alice_ files=[], ) - qtbot.addWidget(w) + qtbot.add_widget(w) with qtbot.waitSignal(w.clicked, timeout=500) as blocker: qtbot.mouseClick(w, QtCore.Qt.LeftButton) assert blocker.args == [workspace_fs] @@ -189,7 +189,7 @@ async def test_workspace_button_share_clicked(qtbot, workspace_fs, core_config, is_mounted=True, files=[], ) - qtbot.addWidget(w) + qtbot.add_widget(w) with qtbot.waitSignal(w.share_clicked, timeout=500) as blocker: qtbot.mouseClick(w.button_share, QtCore.Qt.LeftButton) assert blocker.args == [workspace_fs] @@ -208,7 +208,7 @@ async def test_workspace_button_rename_clicked(qtbot, workspace_fs, core_config, is_mounted=True, files=[], ) - qtbot.addWidget(w) + qtbot.add_widget(w) with qtbot.waitSignal(w.rename_clicked, timeout=500) as blocker: qtbot.mouseClick(w.button_rename, QtCore.Qt.LeftButton) assert blocker.args == [w] @@ -233,7 +233,7 @@ async def test_workspace_button_reencrypt_clicked( user_revoked=True, role_revoked=False, reencryption_already_in_progress=False ) - qtbot.addWidget(w) + qtbot.add_widget(w) assert not w.button_reencrypt.isHidden() @@ -261,7 +261,7 @@ async def test_workspace_button_delete_clicked(qtbot, workspace_fs, core_config, is_mounted=True, files=[], ) - qtbot.addWidget(w) + qtbot.add_widget(w) with qtbot.waitSignal(w.delete_clicked, timeout=500) as blocker: qtbot.mouseClick(w.button_delete, QtCore.Qt.LeftButton) assert blocker.args == [workspace_fs] diff --git a/tests/core/gui/test_workspace_sharing.py b/tests/core/gui/test_workspace_sharing.py index 9775842e602..21a858a574d 100644 --- a/tests/core/gui/test_workspace_sharing.py +++ b/tests/core/gui/test_workspace_sharing.py @@ -28,7 +28,7 @@ async def gui_workspace_sharing( monkeypatch.setattr( "parsec.core.gui.workspaces_widget.get_text_input", lambda *args, **kwargs: ("Workspace") ) - await aqtbot.mouse_click(w_w.button_add_workspace, QtCore.Qt.LeftButton) + aqtbot.mouse_click(w_w.button_add_workspace, QtCore.Qt.LeftButton) def _workspace_added(): assert w_w.layout_workspaces.count() == 1 @@ -42,7 +42,7 @@ def _workspace_added(): await aqtbot.wait_until(_workspace_added, timeout=2000) wk_button = w_w.layout_workspaces.itemAt(0).widget() - await aqtbot.mouse_click(wk_button.button_share, QtCore.Qt.LeftButton) + aqtbot.mouse_click(wk_button.button_share, QtCore.Qt.LeftButton) share_w_w = await catch_share_workspace_widget() yield logged_gui, w_w, share_w_w @@ -152,7 +152,7 @@ def _workspace_listed(): acc_w = accounts_w.accounts_widget.layout().itemAt(i).widget() if acc_w.label_name.text() == user_name: async with aqtbot.wait_signal(accounts_w.account_clicked): - await aqtbot.mouse_click(acc_w, QtCore.Qt.LeftButton) + aqtbot.mouse_click(acc_w, QtCore.Qt.LeftButton) break def _password_widget_shown(): @@ -161,12 +161,12 @@ def _password_widget_shown(): await aqtbot.wait_until(_password_widget_shown) password_w = login_w.widget.layout().itemAt(0).widget() - await aqtbot.key_clicks(password_w.line_edit_password, password) + aqtbot.key_clicks(password_w.line_edit_password, password) tabw = logged_gui.test_get_tab() async with aqtbot.wait_signals([login_w.login_with_password_clicked, tabw.logged_in]): - await aqtbot.mouse_click(password_w.button_login, QtCore.Qt.LeftButton) + aqtbot.mouse_click(password_w.button_login, QtCore.Qt.LeftButton) w_w = await logged_gui.test_switch_to_workspaces_widget() @@ -184,7 +184,7 @@ def _workspace_listed(): assert w_b.workspace_name == "Workspace" assert w_b.is_owner is False - await aqtbot.mouse_click(w_b.button_share, QtCore.Qt.LeftButton) + aqtbot.mouse_click(w_b.button_share, QtCore.Qt.LeftButton) share_w_w = await catch_share_workspace_widget() def _users_listed(): @@ -294,19 +294,19 @@ def _reset_input(): assert _users_visible() == 3 - await aqtbot.key_clicks(share_w_w.line_edit_filter, "face") + aqtbot.key_clicks(share_w_w.line_edit_filter, "face") assert _users_visible() == 3 _reset_input() - await aqtbot.key_clicks(share_w_w.line_edit_filter, "mca") + aqtbot.key_clicks(share_w_w.line_edit_filter, "mca") assert _users_visible() == 2 _reset_input() - await aqtbot.key_clicks(share_w_w.line_edit_filter, "bob") + aqtbot.key_clicks(share_w_w.line_edit_filter, "bob") assert _users_visible() == 1 _reset_input() - await aqtbot.key_clicks(share_w_w.line_edit_filter, "zoidberg") + aqtbot.key_clicks(share_w_w.line_edit_filter, "zoidberg") assert _users_visible() == 0 _reset_input() diff --git a/tests/core/gui/test_workspaces.py b/tests/core/gui/test_workspaces.py index 40d3dad2721..f123e1b000f 100644 --- a/tests/core/gui/test_workspaces.py +++ b/tests/core/gui/test_workspaces.py @@ -31,7 +31,7 @@ async def test_add_workspace( monkeypatch.setattr( "parsec.core.gui.workspaces_widget.get_text_input", lambda *args, **kwargs: (workspace_name) ) - await aqtbot.mouse_click(w_w.button_add_workspace, QtCore.Qt.LeftButton) + aqtbot.mouse_click(w_w.button_add_workspace, QtCore.Qt.LeftButton) def _outcome_occured(): assert w_w.layout_workspaces.count() == 1 @@ -81,7 +81,7 @@ def _workspace_displayed(): monkeypatch.setattr( "parsec.core.gui.workspaces_widget.get_text_input", lambda *args, **kwargs: (workspace_name) ) - await aqtbot.mouse_click(wk_button.button_rename, QtCore.Qt.LeftButton) + aqtbot.mouse_click(wk_button.button_rename, QtCore.Qt.LeftButton) def _outcome_occured(): assert w_w.layout_workspaces.count() == 1 @@ -227,7 +227,7 @@ def _initially_mounted(): await aqtbot.wait_until(_initially_mounted, timeout=3000) # Now switch to umounted - await aqtbot.mouse_click(wk_button.switch_button, QtCore.Qt.LeftButton) + aqtbot.mouse_click(wk_button.switch_button, QtCore.Qt.LeftButton) def _unmounted(): nonlocal wk_button @@ -247,7 +247,7 @@ def _mounted(): assert core.mountpoint_manager.is_workspace_mounted(wid) # Now switch back to mounted - await aqtbot.mouse_click(wk_button.switch_button, QtCore.Qt.LeftButton) + aqtbot.mouse_click(wk_button.switch_button, QtCore.Qt.LeftButton) await aqtbot.wait_until(_mounted, timeout=3000) # Test open button @@ -255,7 +255,7 @@ def _mounted(): def _wk_opened(): open_workspace_mock.assert_called_once() - await aqtbot.mouse_click(wk_button.button_open, QtCore.Qt.LeftButton) + aqtbot.mouse_click(wk_button.button_open, QtCore.Qt.LeftButton) await aqtbot.wait_until(_wk_opened) @@ -319,7 +319,7 @@ def _workspace_filtered(): # Remove filter - await aqtbot.mouse_click(w_w.filter_remove_button, QtCore.Qt.LeftButton) + aqtbot.mouse_click(w_w.filter_remove_button, QtCore.Qt.LeftButton) await aqtbot.wait_until(_workspace_listed, timeout=2000) @@ -381,7 +381,7 @@ def _workspace_filtered(): monkeypatch.setattr( "parsec.core.gui.workspaces_widget.get_text_input", lambda *args, **kwargs: ("Workspace2") ) - await aqtbot.mouse_click(w_w.button_add_workspace, QtCore.Qt.LeftButton) + aqtbot.mouse_click(w_w.button_add_workspace, QtCore.Qt.LeftButton) def _new_workspace_listed(): assert w_w.layout_workspaces.count() == 2 diff --git a/tests/core/gui/test_workspaces_reencrypt.py b/tests/core/gui/test_workspaces_reencrypt.py index b99df24ea34..970ad32f213 100644 --- a/tests/core/gui/test_workspaces_reencrypt.py +++ b/tests/core/gui/test_workspaces_reencrypt.py @@ -136,7 +136,7 @@ async def test_workspace_reencryption( async with aqtbot.wait_signals( [wk_button.button_reencrypt.clicked, wk_button.reencrypt_clicked] ): - await aqtbot.mouse_click(wk_button.button_reencrypt, QtCore.Qt.LeftButton) + aqtbot.mouse_click(wk_button.button_reencrypt, QtCore.Qt.LeftButton) def _reencrypt_button_not_displayed(): assert not wk_button.button_reencrypt.isVisible() @@ -161,7 +161,7 @@ async def test_workspace_reencryption_offline_backend( await display_reencryption_button(aqtbot, monkeypatch, w_w) wk_button = w_w.layout_workspaces.itemAt(0).widget() with running_backend.offline(): - await aqtbot.mouse_click(wk_button.button_reencrypt, QtCore.Qt.LeftButton) + aqtbot.mouse_click(wk_button.button_reencrypt, QtCore.Qt.LeftButton) def _assert_error(): assert len(autoclose_dialog.dialogs) == 1 @@ -192,7 +192,7 @@ async def test_workspace_reencryption_fs_error( wk_button = w_w.layout_workspaces.itemAt(0).widget() await alice_user_fs.workspace_start_reencryption(wk_button.workspace_id) - await aqtbot.mouse_click(wk_button.button_reencrypt, QtCore.Qt.LeftButton) + aqtbot.mouse_click(wk_button.button_reencrypt, QtCore.Qt.LeftButton) def _assert_error(): assert len(autoclose_dialog.dialogs) == 1 @@ -235,7 +235,7 @@ async def test_workspace_reencryption_access_error( reencryption_needed_workspace, alice.user_id, WorkspaceRole.READER ) - await aqtbot.mouse_click(wk_button.button_reencrypt, QtCore.Qt.LeftButton) + aqtbot.mouse_click(wk_button.button_reencrypt, QtCore.Qt.LeftButton) def _assert_error(): assert len(autoclose_dialog.dialogs) == 2 @@ -272,7 +272,7 @@ def mocked_start_reencryption(self, workspace_id): w_w.core.user_fs.workspace_start_reencryption = mocked_start_reencryption.__get__( w_w.core.user_fs ) - await aqtbot.mouse_click(wk_button.button_reencrypt, QtCore.Qt.LeftButton) + aqtbot.mouse_click(wk_button.button_reencrypt, QtCore.Qt.LeftButton) def _assert_error(): assert len(autoclose_dialog.dialogs) == 1 @@ -325,7 +325,7 @@ async def do_one_batch(self, *args, **kwargs): w_w.core.user_fs.workspace_start_reencryption = mocked_start_reencryption.__get__( w_w.core.user_fs ) - await aqtbot.mouse_click(wk_button.button_reencrypt, QtCore.Qt.LeftButton) + aqtbot.mouse_click(wk_button.button_reencrypt, QtCore.Qt.LeftButton) def _assert_error(): assert len(autoclose_dialog.dialogs) == 1 @@ -379,7 +379,7 @@ async def test_workspace_reencryption_continue( async with aqtbot.wait_signals( [wk_button.button_reencrypt.clicked, wk_button.reencrypt_clicked] ): - await aqtbot.mouse_click(wk_button.button_reencrypt, QtCore.Qt.LeftButton) + aqtbot.mouse_click(wk_button.button_reencrypt, QtCore.Qt.LeftButton) def _reencrypt_button_not_displayed(): assert not wk_button.button_reencrypt.isVisible() diff --git a/tests/core/gui/users_widget/test_claim.py b/tests/core/gui/users_widget/test_claim.py index e581b3ed1ad..067581b9d18 100644 --- a/tests/core/gui/users_widget/test_claim.py +++ b/tests/core/gui/users_widget/test_claim.py @@ -151,7 +151,7 @@ def assert_initial_state(self): async def step_1_start_claim(self): cui_w = self.claim_user_instructions_widget - await aqtbot.mouse_click(cui_w.button_start, QtCore.Qt.LeftButton) + aqtbot.mouse_click(cui_w.button_start, QtCore.Qt.LeftButton) def _claimer_started(): assert not cui_w.button_start.isEnabled() @@ -231,11 +231,11 @@ async def step_5_provide_claim_info(self): human_label = self.requested_human_handle.label device_label = self.requested_device_label - await aqtbot.key_clicks(cupi_w.line_edit_user_email, human_email) - await aqtbot.key_clicks(cupi_w.line_edit_user_full_name, human_label) + aqtbot.key_clicks(cupi_w.line_edit_user_email, human_email) + aqtbot.key_clicks(cupi_w.line_edit_user_full_name, human_label) cupi_w.line_edit_device.clear() - await aqtbot.key_clicks(cupi_w.line_edit_device, device_label) - await aqtbot.mouse_click(cupi_w.button_ok, QtCore.Qt.LeftButton) + aqtbot.key_clicks(cupi_w.line_edit_device, device_label) + aqtbot.mouse_click(cupi_w.button_ok, QtCore.Qt.LeftButton) def _claim_info_submitted(): assert not cupi_w.button_ok.isEnabled() @@ -286,11 +286,11 @@ async def step_7_finalize(self): assert not cuf_w.button_finalize.isEnabled() - await aqtbot.key_clicks(cuf_w.widget_password.line_edit_password, self.password) - await aqtbot.key_clicks(cuf_w.widget_password.line_edit_password_check, self.password) + aqtbot.key_clicks(cuf_w.widget_password.line_edit_password, self.password) + aqtbot.key_clicks(cuf_w.widget_password.line_edit_password_check, self.password) assert cuf_w.button_finalize.isEnabled() - await aqtbot.mouse_click(cuf_w.button_finalize, QtCore.Qt.LeftButton) + aqtbot.mouse_click(cuf_w.button_finalize, QtCore.Qt.LeftButton) def _claim_done(): assert not cu_w.isVisible() @@ -347,7 +347,7 @@ async def offline_step_1_start_claim(self): cui_w = self.claim_user_instructions_widget with running_backend.offline(): - await aqtbot.mouse_click(cui_w.button_start, QtCore.Qt.LeftButton) + aqtbot.mouse_click(cui_w.button_start, QtCore.Qt.LeftButton) await aqtbot.wait_until(partial(self._claim_aborted, expected_message)) return None @@ -384,11 +384,11 @@ async def offline_step_5_provide_claim_info(self): device_label = self.requested_device_label with running_backend.offline(): - await aqtbot.key_clicks(cupi_w.line_edit_user_email, human_email) - await aqtbot.key_clicks(cupi_w.line_edit_user_full_name, human_label) + aqtbot.key_clicks(cupi_w.line_edit_user_email, human_email) + aqtbot.key_clicks(cupi_w.line_edit_user_full_name, human_label) cupi_w.line_edit_device.clear() - await aqtbot.key_clicks(cupi_w.line_edit_device, device_label) - await aqtbot.mouse_click(cupi_w.button_ok, QtCore.Qt.LeftButton) + aqtbot.key_clicks(cupi_w.line_edit_device, device_label) + aqtbot.mouse_click(cupi_w.button_ok, QtCore.Qt.LeftButton) await aqtbot.wait_until(partial(self._claim_aborted, expected_message)) return None @@ -464,11 +464,11 @@ async def reset_step_5_provide_claim_info(self): device_label = self.requested_device_label async with self._reset_greeter(): - await aqtbot.key_clicks(cupi_w.line_edit_user_email, human_email) - await aqtbot.key_clicks(cupi_w.line_edit_user_full_name, human_label) + aqtbot.key_clicks(cupi_w.line_edit_user_email, human_email) + aqtbot.key_clicks(cupi_w.line_edit_user_full_name, human_label) cupi_w.line_edit_device.clear() - await aqtbot.key_clicks(cupi_w.line_edit_device, device_label) - await aqtbot.mouse_click(cupi_w.button_ok, QtCore.Qt.LeftButton) + aqtbot.key_clicks(cupi_w.line_edit_device, device_label) + aqtbot.mouse_click(cupi_w.button_ok, QtCore.Qt.LeftButton) await aqtbot.wait_until(partial(self._claim_restart, expected_message)) await self.bootstrap_after_restart() @@ -527,7 +527,7 @@ async def cancelled_step_1_start_claim(self): await self._cancel_invitation() - await aqtbot.mouse_click(cui_w.button_start, QtCore.Qt.LeftButton) + aqtbot.mouse_click(cui_w.button_start, QtCore.Qt.LeftButton) await aqtbot.wait_until(partial(self._claim_restart, expected_message)) return None @@ -567,11 +567,11 @@ async def cancelled_step_5_provide_claim_info(self): await self._cancel_invitation() - await aqtbot.key_clicks(cupi_w.line_edit_user_email, human_email) - await aqtbot.key_clicks(cupi_w.line_edit_user_full_name, human_label) + aqtbot.key_clicks(cupi_w.line_edit_user_email, human_email) + aqtbot.key_clicks(cupi_w.line_edit_user_full_name, human_label) cupi_w.line_edit_device.clear() - await aqtbot.key_clicks(cupi_w.line_edit_device, device_label) - await aqtbot.mouse_click(cupi_w.button_ok, QtCore.Qt.LeftButton) + aqtbot.key_clicks(cupi_w.line_edit_device, device_label) + aqtbot.mouse_click(cupi_w.button_ok, QtCore.Qt.LeftButton) await aqtbot.wait_until(partial(self._claim_restart, expected_message)) return None diff --git a/tests/core/gui/users_widget/test_greet.py b/tests/core/gui/users_widget/test_greet.py index 4333d4ef125..180189fc81f 100644 --- a/tests/core/gui/users_widget/test_greet.py +++ b/tests/core/gui/users_widget/test_greet.py @@ -104,7 +104,7 @@ async def bootstrap(self): # Click on the invitation button - await aqtbot.mouse_click(invitation_widget.button_greet, QtCore.Qt.LeftButton) + aqtbot.mouse_click(invitation_widget.button_greet, QtCore.Qt.LeftButton) greet_user_widget = await catch_greet_user_widget() assert isinstance(greet_user_widget, GreetUserWidget) @@ -163,7 +163,7 @@ def assert_initial_state(self): async def step_1_start_greet(self): gui_w = self.greet_user_information_widget - await aqtbot.mouse_click(gui_w.button_start, QtCore.Qt.LeftButton) + aqtbot.mouse_click(gui_w.button_start, QtCore.Qt.LeftButton) def _greet_started(): assert not gui_w.button_start.isEnabled() @@ -275,7 +275,7 @@ async def step_6_validate_claim_info(self): guci_w = self.greet_user_check_informations_widget # Finally confirm the claimer info and finish the greeting ! - await aqtbot.mouse_click(guci_w.button_create_user, QtCore.Qt.LeftButton) + aqtbot.mouse_click(guci_w.button_create_user, QtCore.Qt.LeftButton) with trio.fail_after(1): await self.claimer_claim_task.join() @@ -334,7 +334,7 @@ async def offline_step_1_start_greet(self): gui_w = self.greet_user_information_widget with running_backend.offline(): - await aqtbot.mouse_click(gui_w.button_start, QtCore.Qt.LeftButton) + aqtbot.mouse_click(gui_w.button_start, QtCore.Qt.LeftButton) await aqtbot.wait_until(partial(self._greet_aborted, expected_message)) return None @@ -376,7 +376,7 @@ async def offline_step_6_validate_claim_info(self): with running_backend.offline(): self.nursery.cancel_scope.cancel() - await aqtbot.mouse_click(guci_w.button_create_user, QtCore.Qt.LeftButton) + aqtbot.mouse_click(guci_w.button_create_user, QtCore.Qt.LeftButton) await aqtbot.wait_until(partial(self._greet_aborted, expected_message)) return None @@ -450,7 +450,7 @@ async def reset_step_6_validate_claim_info(self): await self.claimer_claim_task.cancel_and_join() async with self._reset_claimer(): - await aqtbot.mouse_click(guci_w.button_create_user, QtCore.Qt.LeftButton) + aqtbot.mouse_click(guci_w.button_create_user, QtCore.Qt.LeftButton) await aqtbot.wait_until(partial(self._greet_restart, expected_message)) await self.bootstrap_after_restart() @@ -500,7 +500,7 @@ async def cancelled_step_1_start_greet(self): await self._cancel_invitation() - await aqtbot.mouse_click(gui_w.button_start, QtCore.Qt.LeftButton) + aqtbot.mouse_click(gui_w.button_start, QtCore.Qt.LeftButton) await aqtbot.wait_until(partial(self._greet_restart, expected_message)) return None @@ -546,7 +546,7 @@ async def cancelled_step_6_validate_claim_info(self): await self.claimer_claim_task.cancel_and_join() await self._cancel_invitation() - await aqtbot.mouse_click(guci_w.button_create_user, QtCore.Qt.LeftButton) + aqtbot.mouse_click(guci_w.button_create_user, QtCore.Qt.LeftButton) await aqtbot.wait_until(partial(self._greet_restart, expected_message)) return None diff --git a/tests/core/gui/users_widget/test_invite.py b/tests/core/gui/users_widget/test_invite.py index cda0c568e2f..7f129e74fba 100644 --- a/tests/core/gui/users_widget/test_invite.py +++ b/tests/core/gui/users_widget/test_invite.py @@ -27,7 +27,7 @@ async def test_invite_user( ) if online: - await aqtbot.mouse_click(u_w.button_add_user, QtCore.Qt.LeftButton) + aqtbot.mouse_click(u_w.button_add_user, QtCore.Qt.LeftButton) def _new_invitation_displayed(): assert u_w.layout_users.count() == 4 @@ -52,7 +52,7 @@ def _new_invitation_displayed(): "parsec.core.gui.users_widget.get_text_input", lambda *args, **kwargs: bob.human_handle.email, ) - await aqtbot.mouse_click(u_w.button_add_user, QtCore.Qt.LeftButton) + aqtbot.mouse_click(u_w.button_add_user, QtCore.Qt.LeftButton) def _already_member(): assert autoclose_dialog.dialogs == [ @@ -63,7 +63,7 @@ def _already_member(): else: with running_backend.offline(): - await aqtbot.mouse_click(u_w.button_add_user, QtCore.Qt.LeftButton) + aqtbot.mouse_click(u_w.button_add_user, QtCore.Qt.LeftButton) def _email_send_failed(): assert autoclose_dialog.dialogs == [ @@ -185,7 +185,7 @@ async def test_cancel_user_invitation( u_w = await logged_gui.test_switch_to_users_widget() # Invite new user - await aqtbot.mouse_click(u_w.button_add_user, QtCore.Qt.LeftButton) + aqtbot.mouse_click(u_w.button_add_user, QtCore.Qt.LeftButton) def _new_invitation_displayed(): assert u_w.layout_users.count() == 4 @@ -202,7 +202,7 @@ def _new_invitation_displayed(): assert user_invitation_w.email == email # Cancel invitation - await aqtbot.mouse_click(user_invitation_w.button_cancel, QtCore.Qt.LeftButton) + aqtbot.mouse_click(user_invitation_w.button_cancel, QtCore.Qt.LeftButton) def _new_invitation_removed(): assert u_w.layout_users.count() == 3 diff --git a/tests/core/gui/users_widget/test_list.py b/tests/core/gui/users_widget/test_list.py index 4d79c7ded0c..a546a51ca95 100644 --- a/tests/core/gui/users_widget/test_list.py +++ b/tests/core/gui/users_widget/test_list.py @@ -129,8 +129,8 @@ def _all_users_visible(u_w): await aqtbot.wait_until(lambda: _all_users_visible(u_w=u_w)) async with aqtbot.wait_signal(u_w.list_success): - await aqtbot.key_clicks(u_w.line_edit_search, "bo") - await aqtbot.mouse_click(u_w.button_users_filter, QtCore.Qt.LeftButton) + aqtbot.key_clicks(u_w.line_edit_search, "bo") + aqtbot.mouse_click(u_w.button_users_filter, QtCore.Qt.LeftButton) await aqtbot.wait_until(lambda: _users_shown(count=1)) @@ -148,8 +148,8 @@ def _all_users_visible(u_w): # Test find() async with aqtbot.wait_signal(u_w.list_success): - await aqtbot.key_clicks(u_w.line_edit_search, "McA") - await aqtbot.key_press(u_w.line_edit_search, Qt.Key_Enter) + aqtbot.key_clicks(u_w.line_edit_search, "McA") + aqtbot.key_press(u_w.line_edit_search, Qt.Key_Enter) assert u_w.layout_users.count() == 2 From 6b23660ff3a4f22ce8870549a6f30a935afd2345 Mon Sep 17 00:00:00 2001 From: Vincent Michel Date: Thu, 29 Jul 2021 12:03:31 +0200 Subject: [PATCH 26/35] Use an async exit stack for aqtbot.wait_signals --- parsec/core/gui/trio_thread.py | 2 +- tests/core/gui/conftest.py | 17 +++++------------ 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/parsec/core/gui/trio_thread.py b/parsec/core/gui/trio_thread.py index 52d5edbbb70..a94eef51227 100644 --- a/parsec/core/gui/trio_thread.py +++ b/parsec/core/gui/trio_thread.py @@ -1,8 +1,8 @@ # Parsec Cloud (https://parsec.cloud) Copyright (c) AGPLv3 2016-2021 Scille SAS import threading -from contextlib import asynccontextmanager from inspect import iscoroutinefunction, signature +from async_generator import asynccontextmanager import trio from structlog import get_logger diff --git a/tests/core/gui/conftest.py b/tests/core/gui/conftest.py index d0dc626dd55..e20ad9fd871 100644 --- a/tests/core/gui/conftest.py +++ b/tests/core/gui/conftest.py @@ -2,7 +2,8 @@ import time from importlib import import_module -from contextlib import asynccontextmanager +from async_exit_stack import AsyncExitStack +from async_generator import asynccontextmanager import trio import qtrio @@ -78,20 +79,12 @@ def timed_out(): raise TimeoutError(timeout_msg) await self.wait(10) - @asynccontextmanager - async def _wait_signals(self, signals): - if not signals: - yield - return - head, *tail = signals - async with qtrio._core.wait_signal_context(head): - async with self.wait_signals(tail): - yield - @asynccontextmanager async def wait_signals(self, signals, *, timeout=5000): with trio.fail_after(timeout / 1000): - async with self._wait_signals(signals): + async with AsyncExitStack() as stack: + for signal in signals: + await stack.enter_async_context(qtrio._core.wait_signal_context(signal)) yield @asynccontextmanager From 006d0ed820ca1ca6c8819c79dc7353eaa0684477 Mon Sep 17 00:00:00 2001 From: Vincent Michel Date: Thu, 29 Jul 2021 12:10:18 +0200 Subject: [PATCH 27/35] Address a few of @touilleMan's comments --- tests/core/gui/conftest.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/tests/core/gui/conftest.py b/tests/core/gui/conftest.py index e20ad9fd871..0b5fa1a22df 100644 --- a/tests/core/gui/conftest.py +++ b/tests/core/gui/conftest.py @@ -56,10 +56,10 @@ async def wait_until(self, callback, *, timeout=5000): """ __tracebackhide__ = True - start = time.time() + start = trio.current_time() def timed_out(): - elapsed = time.time() - start + elapsed = trio.current_time() - start elapsed_ms = elapsed * 1000 return elapsed_ms > timeout @@ -225,16 +225,12 @@ async def _gui_factory( gui_show_confined=False, ) event_bus = event_bus or event_bus_factory() - # Language config rely on global var, must reset it for each test ! - switch_language(core_config) - ParsecApp.connected_devices = set() - # Pass minimize_on_close to avoid having test blocked by the - # closing confirmation prompt - + # Language config rely on global var, must reset it for each test ! switch_language(core_config, "en") + # Pass minimize_on_close to avoid having test blocked by the closing confirmation prompt main_w = testing_main_window_cls( job_scheduler, job_scheduler.close, event_bus, core_config, minimize_on_close=True ) From 09f0920106869c4abe7191dc962077561eff28cb Mon Sep 17 00:00:00 2001 From: Vincent Michel Date: Thu, 29 Jul 2021 12:22:54 +0200 Subject: [PATCH 28/35] Better implementation for wait_exposed and wait_active methods --- tests/core/gui/conftest.py | 33 ++++++++++++++------------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/tests/core/gui/conftest.py b/tests/core/gui/conftest.py index 0b5fa1a22df..678cb2c2421 100644 --- a/tests/core/gui/conftest.py +++ b/tests/core/gui/conftest.py @@ -1,6 +1,5 @@ # Parsec Cloud (https://parsec.cloud) Copyright (c) AGPLv3 2016-2021 Scille SAS -import time from importlib import import_module from async_exit_stack import AsyncExitStack from async_generator import asynccontextmanager @@ -8,8 +7,8 @@ import trio import qtrio import pytest -from PyQt5 import QtCore -from pytestqt import exceptions as pytestqt_exceptions +from PyQt5 import QtCore, QtTest +from pytestqt.exceptions import TimeoutError from parsec import __version__ as parsec_version from parsec.core.local_device import save_device_with_password @@ -94,29 +93,25 @@ async def wait_signal(self, signal, *, timeout=5000): @asynccontextmanager async def wait_active(self, widget, *, timeout=5000): + deadline = trio.current_time() + timeout / 1000 yield - deadline = time.time() + timeout / 1000 - while time.time() < deadline: - try: - with self.qtbot.wait_active(widget, timeout=10): - await trio.sleep(0.01) - except pytestqt_exceptions.TimeoutError: - continue - else: + while True: + if QtTest.QTest.qWaitForWindowActive(widget, 10): return + if trio.current_time() > deadline: + raise TimeoutError + await trio.sleep(0.010) @asynccontextmanager async def wait_exposed(self, widget, *, timeout=5000): + deadline = trio.current_time() + timeout / 1000 yield - deadline = time.time() + timeout / 1000 - while time.time() < deadline: - try: - with self.qtbot.wait_exposed(widget, timeout=10): - await trio.sleep(0.01) - except pytestqt_exceptions.TimeoutError: - continue - else: + while True: + if QtTest.QTest.qWaitForWindowExposed(widget, 10): return + if trio.current_time() > deadline: + raise TimeoutError + await trio.sleep(0.010) @pytest.fixture From 17ecb519c44fdb3e0c33e37e41f855ee6e74f4fd Mon Sep 17 00:00:00 2001 From: Vincent Michel Date: Thu, 29 Jul 2021 12:36:54 +0200 Subject: [PATCH 29/35] Add a comment about job signals --- parsec/core/gui/trio_thread.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/parsec/core/gui/trio_thread.py b/parsec/core/gui/trio_thread.py index a94eef51227..c9121cba976 100644 --- a/parsec/core/gui/trio_thread.py +++ b/parsec/core/gui/trio_thread.py @@ -120,6 +120,9 @@ def set_exception(self, exc): def _set_done(self): self._done.set() signal = self._qt_on_success if self.is_ok() else self._qt_on_error + # TODO: Either pick a consistent API for those signals or get rid of the altogether. + # In the meantime, hack into the signal time in order to detect whether the job should + # be emitted as an argument to the signal if signal.signal.endswith("(PyQt_PyObject)"): signal.emit(self) else: From dcf4551ed6e4d670376645dccdeefc7d8a8f98b5 Mon Sep 17 00:00:00 2001 From: Vincent Michel Date: Thu, 29 Jul 2021 13:00:33 +0200 Subject: [PATCH 30/35] Address some of @touilleMan's comments --- tests/core/gui/conftest.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/tests/core/gui/conftest.py b/tests/core/gui/conftest.py index 678cb2c2421..1b67e0e8e2c 100644 --- a/tests/core/gui/conftest.py +++ b/tests/core/gui/conftest.py @@ -241,8 +241,7 @@ def right_main_window(): # For some reasons, the main window from the previous test might # still be around. Simply wait for things to settle down until # our freshly created window is detected as the app main window. - # TODO: investigate why `await aqtbot.wait_until(right_main_window)` fails - aqtbot.qtbot.wait_until(right_main_window) + await aqtbot.wait_until(right_main_window) return main_w @@ -413,13 +412,17 @@ async def test_switch_to_files_widget(self, workspace_name, error=False): raise AssertionError(f"Workspace `{workspace_name}` not found") f_w = self.test_get_files_widget() - async with aqtbot.wait_exposed(f_w), aqtbot.wait_signal(f_w.folder_changed): - # We need to make sure the workspace button is ready for left click first - await aqtbot.wait_until(wk_button.switch_button.isChecked) - # Send the click + + # We need to make sure the workspace button is ready for left click first + async with aqtbot.wait_exposed(f_w): + pass + await aqtbot.wait_until(wk_button.switch_button.isChecked) + + # Send the click and wait for the folder changed signal + async with aqtbot.wait_signal(f_w.folder_changed): aqtbot.mouse_click(wk_button, QtCore.Qt.LeftButton) - # Wait for the spinner to disappear + # Wait for the spinner to disappear, meaning the folder information is properly displayed await aqtbot.wait_until(f_w.spinner.isHidden) return f_w From 4cf9ec62d0abccf65e5377083c1512d05fc80493 Mon Sep 17 00:00:00 2001 From: Vincent Michel Date: Thu, 29 Jul 2021 18:57:47 +0200 Subject: [PATCH 31/35] Remove useless intermediate qt signals for parsec events --- parsec/core/gui/app.py | 8 +-- parsec/core/gui/central_widget.py | 11 +--- parsec/core/gui/files_widget.py | 63 +++++++------------ parsec/core/gui/mount_widget.py | 2 +- parsec/core/gui/workspaces_widget.py | 90 ++++++++-------------------- tests/core/gui/test_workspaces.py | 60 +------------------ 6 files changed, 55 insertions(+), 179 deletions(-) diff --git a/parsec/core/gui/app.py b/parsec/core/gui/app.py index e502aa56f14..9d7e3367dfd 100644 --- a/parsec/core/gui/app.py +++ b/parsec/core/gui/app.py @@ -48,14 +48,14 @@ def _before_quit(): async def _run_ipc_server(config, main_window, start_arg, task_status=trio.TASK_STATUS_IGNORED): - new_instance_needed_qt = main_window.new_instance_needed - foreground_needed_qt = main_window.foreground_needed + new_instance_needed = main_window.new_instance_needed + foreground_needed = main_window.foreground_needed async def _cmd_handler(cmd): if cmd["cmd"] == IPCCommand.FOREGROUND: - foreground_needed_qt.emit() + foreground_needed.emit() elif cmd["cmd"] == IPCCommand.NEW_INSTANCE: - new_instance_needed_qt.emit(cmd.get("start_arg")) + new_instance_needed.emit(cmd.get("start_arg")) return {"status": "ok"} # Loop over attemps at running an IPC server or sending the command to an existing one diff --git a/parsec/core/gui/central_widget.py b/parsec/core/gui/central_widget.py index 3a637db5895..e08227e7b65 100644 --- a/parsec/core/gui/central_widget.py +++ b/parsec/core/gui/central_widget.py @@ -78,7 +78,6 @@ class CentralWidget(QWidget, Ui_CentralWidget): # type: ignore[misc] organization_stats_error = pyqtSignal(QtToTrioJob) connection_state_changed = pyqtSignal(object, object) - vlobs_updated_qt = pyqtSignal() logout_requested = pyqtSignal() new_notification = pyqtSignal(str, str) @@ -107,9 +106,8 @@ def __init__( for e in self.NOTIFICATION_EVENTS: self.event_bus.connect(e, cast(EventCallback, self.handle_event)) - self.event_bus.connect(CoreEvent.FS_ENTRY_SYNCED, self._on_vlobs_updated_trio) - self.event_bus.connect(CoreEvent.BACKEND_REALM_VLOBS_UPDATED, self._on_vlobs_updated_trio) - self.vlobs_updated_qt.connect(self._on_vlobs_updated_qt) + self.event_bus.connect(CoreEvent.FS_ENTRY_SYNCED, self._on_vlobs_updated) + self.event_bus.connect(CoreEvent.BACKEND_REALM_VLOBS_UPDATED, self._on_vlobs_updated) self.set_user_info() menu = QMenu() @@ -270,10 +268,7 @@ def _load_organization_stats(self, delay: float = 0) -> None: core=self.core, ) - def _on_vlobs_updated_trio(self, *args: object, **kwargs: object) -> None: - self.vlobs_updated_qt.emit() - - def _on_vlobs_updated_qt(self) -> None: + def _on_vlobs_updated(self, *args: object, **kwargs: object) -> None: self._load_organization_stats(delay=self.REFRESH_ORGANIZATION_STATS_DELAY) def _on_connection_state_changed( diff --git a/parsec/core/gui/files_widget.py b/parsec/core/gui/files_widget.py index 3c61a7e5ad8..28bc2b92d57 100644 --- a/parsec/core/gui/files_widget.py +++ b/parsec/core/gui/files_widget.py @@ -3,14 +3,13 @@ import pathlib from parsec.core.core_events import CoreEvent -from uuid import UUID from pendulum import DateTime from enum import IntEnum from structlog import get_logger from PyQt5.QtCore import Qt, pyqtSignal from PyQt5.QtWidgets import QFileDialog, QWidget -from parsec.core.types import FsPath, WorkspaceEntry, WorkspaceRole +from parsec.core.types import FsPath, WorkspaceRole from parsec.core.fs import WorkspaceFS, WorkspaceFSTimestamped from parsec.core.fs.exceptions import ( FSRemoteManifestNotFound, @@ -250,12 +249,8 @@ def __init__(self, files, status, source_workspace=None): class FilesWidget(QWidget, Ui_FilesWidget): RELOAD_FILES_LIST_THROTTLE_DELAY = 1 # 1s - fs_updated_qt = pyqtSignal(CoreEvent, UUID) - fs_synced_qt = pyqtSignal(CoreEvent, UUID) - entry_downsynced_qt = pyqtSignal(UUID, UUID) - global_clipboard_updated_qt = pyqtSignal(object) + global_clipboard_updated = pyqtSignal(object) - sharing_updated_qt = pyqtSignal(WorkspaceEntry, object) back_clicked = pyqtSignal() rename_success = pyqtSignal(QtToTrioJob) @@ -312,9 +307,6 @@ def __init__(self, core, jobs_ctx, event_bus, *args, **kwargs): self.line_edit_search.hide() self.current_directory = FsPath("/") self.current_directory_uuid = None - self.fs_updated_qt.connect(self._on_fs_updated_qt) - self.fs_synced_qt.connect(self._on_fs_synced_qt) - self.entry_downsynced_qt.connect(self._on_entry_downsynced_qt) self.default_import_path = str(pathlib.Path.home()) self.table_files.config = self.core.config self.table_files.file_moved.connect(self.on_table_files_file_moved) @@ -330,7 +322,6 @@ def __init__(self, core, jobs_ctx, event_bus, *args, **kwargs): self.table_files.file_path_clicked.connect(self.on_get_file_path_clicked) self.table_files.open_current_dir_clicked.connect(self.on_open_current_dir_clicked) - self.sharing_updated_qt.connect(self._on_sharing_updated_qt) self.rename_success.connect(self._on_rename_success) self.rename_error.connect(self._on_rename_error) self.delete_success.connect(self._on_delete_success) @@ -353,10 +344,10 @@ def __init__(self, core, jobs_ctx, event_bus, *args, **kwargs): self.loading_dialog = None self.import_progress.connect(self._on_import_progress) - self.event_bus.connect(CoreEvent.FS_ENTRY_UPDATED, self._on_fs_entry_updated_trio) - self.event_bus.connect(CoreEvent.FS_ENTRY_SYNCED, self._on_fs_entry_synced_trio) - self.event_bus.connect(CoreEvent.SHARING_UPDATED, self._on_sharing_updated_trio) - self.event_bus.connect(CoreEvent.FS_ENTRY_DOWNSYNCED, self._on_entry_downsynced_trio) + self.event_bus.connect(CoreEvent.FS_ENTRY_SYNCED, self._on_fs_entry_synced) + self.event_bus.connect(CoreEvent.FS_ENTRY_UPDATED, self._on_fs_entry_updated) + self.event_bus.connect(CoreEvent.FS_ENTRY_DOWNSYNCED, self._on_fs_entry_downsynced) + self.event_bus.connect(CoreEvent.SHARING_UPDATED, self._on_sharing_updated) def disconnect_all(self): pass @@ -420,7 +411,7 @@ def on_copy_clicked(self): self.clipboard = Clipboard( files=files_to_copy, status=Clipboard.Status.Copied, source_workspace=self.workspace_fs ) - self.global_clipboard_updated_qt.emit(self.clipboard) + self.global_clipboard_updated.emit(self.clipboard) self.table_files.paste_status = PasteStatus(status=PasteStatus.Status.Enabled) def on_cut_clicked(self): @@ -437,7 +428,7 @@ def on_cut_clicked(self): self.clipboard = Clipboard( files=files_to_cut, status=Clipboard.Status.Cut, source_workspace=self.workspace_fs ) - self.global_clipboard_updated_qt.emit(self.clipboard) + self.global_clipboard_updated.emit(self.clipboard) self.table_files.paste_status = PasteStatus(status=PasteStatus.Status.Enabled) def on_paste_clicked(self): @@ -456,7 +447,7 @@ def on_paste_clicked(self): ) self.clipboard = None # Set Global clipboard to none too - self.global_clipboard_updated_qt.emit(None) + self.global_clipboard_updated.emit(None) self.table_files.paste_status = PasteStatus(status=PasteStatus.Status.Disabled) elif self.clipboard.status == Clipboard.Status.Copied: @@ -979,20 +970,7 @@ def _display_import_error(file_count, exceptions=None): self.loading_dialog = None self.import_job = None - def _on_fs_entry_synced_trio(self, event, id, workspace_id=None): - self.fs_synced_qt.emit(event, id) - - def _on_fs_entry_updated_trio(self, event, workspace_id=None, id=None): - assert id is not None - if workspace_id is None or ( - self.workspace_fs is not None and workspace_id == self.workspace_fs.workspace_id - ): - self.fs_updated_qt.emit(event, id) - - def _on_entry_downsynced_trio(self, event, workspace_id=None, id=None): - self.entry_downsynced_qt.emit(workspace_id, id) - - def _on_entry_downsynced_qt(self, workspace_id, id): + def _on_fs_entry_downsynced(self, event, workspace_id=None, id=None): if not self.workspace_fs: return ws_id = self.workspace_fs.workspace_id @@ -1001,16 +979,16 @@ def _on_entry_downsynced_qt(self, workspace_id, id): if id == self.current_directory_uuid: self.reload() - def _on_fs_synced_qt(self, event, uuid): + def _on_fs_entry_synced(self, event, id, workspace_id=None): if not self.workspace_fs: return - if self.current_directory_uuid == uuid: + if self.current_directory_uuid == id: return for i in range(1, self.table_files.rowCount()): item = self.table_files.item(i, 0) - if item and item.data(UUID_DATA_INDEX) == uuid: + if item and item.data(UUID_DATA_INDEX) == id: if ( item.data(TYPE_DATA_INDEX) == FileType.File or item.data(TYPE_DATA_INDEX) == FileType.Folder @@ -1018,17 +996,16 @@ def _on_fs_synced_qt(self, event, uuid): item.confined = False item.is_synced = True - def _on_fs_updated_qt(self, event, uuid): - if not self.workspace_fs: + def _on_fs_entry_updated(self, event, workspace_id=None, id=None): + assert id is not None + if self.workspace_fs is None: return - - if self.current_directory_uuid == uuid or self.table_files.has_file(uuid): + if workspace_id != self.workspace_fs.workspace_id: + return + if self.current_directory_uuid == id or self.table_files.has_file(id): self.reload() - def _on_sharing_updated_trio(self, event, new_entry, previous_entry): - self.sharing_updated_qt.emit(new_entry, previous_entry) - - def _on_sharing_updated_qt(self, new_entry, previous_entry): + def _on_sharing_updated(self, event, new_entry, previous_entry): if new_entry is None or new_entry.role is None: # Sharing revoked show_error( diff --git a/parsec/core/gui/mount_widget.py b/parsec/core/gui/mount_widget.py index 19ac1673c26..a9ae02b5116 100644 --- a/parsec/core/gui/mount_widget.py +++ b/parsec/core/gui/mount_widget.py @@ -29,7 +29,7 @@ def __init__(self, core, jobs_ctx, event_bus, **kwargs): self.workspaces_widget.load_workspace_clicked.connect(self.load_workspace) self.files_widget = FilesWidget(self.core, self.jobs_ctx, self.event_bus, parent=self) self.files_widget.folder_changed.connect(self.folder_changed.emit) - self.files_widget.global_clipboard_updated_qt.connect(self.clipboard_updated) + self.files_widget.global_clipboard_updated.connect(self.clipboard_updated) self.layout_content.insertWidget(0, self.files_widget) self.files_widget.back_clicked.connect(self.show_workspaces_widget) self.show_workspaces_widget() diff --git a/parsec/core/gui/workspaces_widget.py b/parsec/core/gui/workspaces_widget.py index c8947af669b..70a94e95fd2 100644 --- a/parsec/core/gui/workspaces_widget.py +++ b/parsec/core/gui/workspaces_widget.py @@ -1,7 +1,6 @@ # Parsec Cloud (https://parsec.cloud) Copyright (c) AGPLv3 2016-2021 Scille SAS from parsec.core.core_events import CoreEvent -from uuid import UUID from PyQt5.QtCore import pyqtSignal, Qt from PyQt5.QtWidgets import QWidget, QLabel @@ -11,7 +10,6 @@ from contextlib import contextmanager from parsec.core.types import ( - WorkspaceEntry, UserInfo, FsPath, EntryID, @@ -158,12 +156,6 @@ async def _do_workspace_unmount(core, workspace_id, timestamp: pendulum.DateTime class WorkspacesWidget(QWidget, Ui_WorkspacesWidget): REFRESH_WORKSPACES_LIST_DELAY = 1 # 1s - fs_updated_qt = pyqtSignal(CoreEvent, UUID) - fs_synced_qt = pyqtSignal(CoreEvent, UUID) - entry_downsynced_qt = pyqtSignal(UUID, UUID) - - sharing_updated_qt = pyqtSignal(WorkspaceEntry, object) - _workspace_created_qt = pyqtSignal(WorkspaceEntry) load_workspace_clicked = pyqtSignal(WorkspaceFS, FsPath, bool) workspace_reencryption_success = pyqtSignal(QtToTrioJob) workspace_reencryption_error = pyqtSignal(QtToTrioJob) @@ -208,10 +200,6 @@ def __init__(self, core, jobs_ctx, event_bus, **kwargs): self.button_add_workspace.apply_style() self.button_goto_file.apply_style() - self.fs_updated_qt.connect(self._on_fs_updated_qt) - self.fs_synced_qt.connect(self._on_fs_synced_qt) - self.entry_downsynced_qt.connect(self._on_entry_downsynced_qt) - self.line_edit_search.textChanged.connect(self.on_workspace_filter) self.rename_success.connect(self.on_rename_success) @@ -234,11 +222,6 @@ def __init__(self, core, jobs_ctx, event_bus, **kwargs): self.filter_remove_button.clicked.connect(self.remove_user_filter) self.filter_remove_button.apply_style() - self.mountpoint_state_updated.connect(self._on_mountpoint_state_updated_qt) - - self.sharing_updated_qt.connect(self._on_sharing_updated_qt) - self._workspace_created_qt.connect(self._on_workspace_created_qt) - self.filter_user_info = None self.filter_layout_widget.hide() @@ -258,30 +241,24 @@ def disconnect_all(self): pass def showEvent(self, event): - self.event_bus.connect(CoreEvent.FS_WORKSPACE_CREATED, self._on_workspace_created_trio) - self.event_bus.connect(CoreEvent.FS_ENTRY_UPDATED, self._on_fs_entry_updated_trio) - self.event_bus.connect(CoreEvent.FS_ENTRY_SYNCED, self._on_fs_entry_synced_trio) - self.event_bus.connect(CoreEvent.SHARING_UPDATED, self._on_sharing_updated_trio) - self.event_bus.connect(CoreEvent.FS_ENTRY_DOWNSYNCED, self._on_entry_downsynced_trio) - self.event_bus.connect(CoreEvent.MOUNTPOINT_STARTED, self._on_mountpoint_started_trio) - self.event_bus.connect(CoreEvent.MOUNTPOINT_STOPPED, self._on_mountpoint_stopped_trio) + self.event_bus.connect(CoreEvent.FS_WORKSPACE_CREATED, self._on_workspace_created) + self.event_bus.connect(CoreEvent.FS_ENTRY_UPDATED, self._on_fs_entry_updated) + self.event_bus.connect(CoreEvent.FS_ENTRY_SYNCED, self._on_fs_entry_synced) + self.event_bus.connect(CoreEvent.SHARING_UPDATED, self._on_sharing_updated) + self.event_bus.connect(CoreEvent.FS_ENTRY_DOWNSYNCED, self._on_entry_downsynced) + self.event_bus.connect(CoreEvent.MOUNTPOINT_STARTED, self._on_mountpoint_started) + self.event_bus.connect(CoreEvent.MOUNTPOINT_STOPPED, self._on_mountpoint_stopped) self.reset() def hideEvent(self, event): try: - self.event_bus.disconnect( - CoreEvent.FS_WORKSPACE_CREATED, self._on_workspace_created_trio - ) - self.event_bus.disconnect(CoreEvent.FS_ENTRY_UPDATED, self._on_fs_entry_updated_trio) - self.event_bus.disconnect(CoreEvent.FS_ENTRY_SYNCED, self._on_fs_entry_synced_trio) - self.event_bus.disconnect(CoreEvent.SHARING_UPDATED, self._on_sharing_updated_trio) - self.event_bus.disconnect(CoreEvent.FS_ENTRY_DOWNSYNCED, self._on_entry_downsynced_trio) - self.event_bus.disconnect( - CoreEvent.MOUNTPOINT_STARTED, self._on_mountpoint_started_trio - ) - self.event_bus.disconnect( - CoreEvent.MOUNTPOINT_STOPPED, self._on_mountpoint_stopped_trio - ) + self.event_bus.disconnect(CoreEvent.FS_WORKSPACE_CREATED, self._on_workspace_created) + self.event_bus.disconnect(CoreEvent.FS_ENTRY_UPDATED, self._on_fs_entry_updated) + self.event_bus.disconnect(CoreEvent.FS_ENTRY_SYNCED, self._on_fs_entry_synced) + self.event_bus.disconnect(CoreEvent.SHARING_UPDATED, self._on_sharing_updated) + self.event_bus.disconnect(CoreEvent.FS_ENTRY_DOWNSYNCED, self._on_entry_downsynced) + self.event_bus.disconnect(CoreEvent.MOUNTPOINT_STARTED, self._on_mountpoint_started) + self.event_bus.disconnect(CoreEvent.MOUNTPOINT_STOPPED, self._on_mountpoint_stopped) except ValueError: pass @@ -767,46 +744,31 @@ def list_workspaces(self): core=self.core, ) - def _on_sharing_updated_trio(self, event, new_entry, previous_entry): - self.sharing_updated_qt.emit(new_entry, previous_entry) - - def _on_sharing_updated_qt(self, new_entry, previous_entry): + def _on_sharing_updated(self, event, new_entry, previous_entry): self.reset() - def _on_workspace_created_trio(self, event, new_entry): - self._workspace_created_qt.emit(new_entry) - - def _on_workspace_created_qt(self, workspace_entry): + def _on_workspace_created(self, event, new_entry): self.reset() - def _on_fs_entry_synced_trio(self, event, id, workspace_id=None): - self.fs_synced_qt.emit(event, id) + def _on_fs_entry_synced(self, event, id, workspace_id=None): + self.reset() - def _on_fs_entry_updated_trio(self, event, workspace_id=None, id=None): + def _on_fs_entry_updated(self, event, workspace_id=None, id=None): assert id is not None if workspace_id and id == workspace_id: - self.fs_updated_qt.emit(event, workspace_id) - - def _on_entry_downsynced_trio(self, event, workspace_id=None, id=None): - self.entry_downsynced_qt.emit(workspace_id, id) - - def _on_entry_downsynced_qt(self, workspace_id, id): - self.reset() - - def _on_fs_synced_qt(self, event, id): - self.reset() + self.reset() - def _on_fs_updated_qt(self, event, workspace_id): + def _on_entry_downsynced(self, event, workspace_id=None, id=None): self.reset() - def _on_mountpoint_state_updated_qt(self, workspace_id, timestamp): + def _on_mountpoint_state_updated(self, workspace_id, timestamp): wb = self.get_workspace_button(workspace_id, timestamp) if wb: mounted = self.is_workspace_mounted(workspace_id, timestamp) wb.set_mountpoint_state(mounted) - def _on_mountpoint_started_trio(self, event, mountpoint, workspace_id, timestamp): - self.mountpoint_state_updated.emit(workspace_id, timestamp) + def _on_mountpoint_started(self, event, mountpoint, workspace_id, timestamp): + self._on_mountpoint_state_updated(workspace_id, timestamp) - def _on_mountpoint_stopped_trio(self, event, mountpoint, workspace_id, timestamp): - self.mountpoint_state_updated.emit(workspace_id, timestamp) + def _on_mountpoint_stopped(self, event, mountpoint, workspace_id, timestamp): + self._on_mountpoint_state_updated(workspace_id, timestamp) diff --git a/tests/core/gui/test_workspaces.py b/tests/core/gui/test_workspaces.py index f123e1b000f..6cf60ab4be5 100644 --- a/tests/core/gui/test_workspaces.py +++ b/tests/core/gui/test_workspaces.py @@ -3,11 +3,8 @@ import pytest from PyQt5 import QtCore -from uuid import UUID -import pendulum -from unittest.mock import ANY, Mock +from unittest.mock import Mock -from parsec.api.data import WorkspaceEntry from parsec.core.types import WorkspaceRole from parsec.core.core_events import CoreEvent from parsec.core.fs import FSWorkspaceNoReadAccess @@ -136,61 +133,6 @@ async def test_mountpoint_remote_error_event(aqtbot, running_backend, logged_gui ) -@pytest.mark.skip("Should be reworked") -@pytest.mark.gui -@pytest.mark.trio -async def test_event_bus_internal_connection(aqtbot, running_backend, logged_gui, autoclose_dialog): - w_w = await logged_gui.test_switch_to_workspaces_widget() - uuid = UUID("1bc1e17b-157a-462f-86f2-7f64657ba16a") - w_entry = WorkspaceEntry( - name="w", - id=ANY, - key=ANY, - encryption_revision=1, - encrypted_on=ANY, - role_cached_on=ANY, - role=None, - ) - - async with aqtbot.wait_signal(w_w.fs_synced_qt): - w_w.event_bus.send(CoreEvent.FS_ENTRY_SYNCED, workspace_id=None, id=uuid) - - async with aqtbot.wait_signal(w_w.fs_updated_qt): - w_w.event_bus.send(CoreEvent.FS_ENTRY_UPDATED, workspace_id=uuid, id=None) - - async with aqtbot.wait_signal(w_w._workspace_created_qt): - w_w.event_bus.send(CoreEvent.FS_WORKSPACE_CREATED, new_entry=w_entry) - - async with aqtbot.wait_signal(w_w.sharing_updated_qt): - w_w.event_bus.send(CoreEvent.SHARING_UPDATED, new_entry=w_entry, previous_entry=None) - - async with aqtbot.wait_signal(w_w.entry_downsynced_qt): - w_w.event_bus.send(CoreEvent.FS_ENTRY_DOWNSYNCED, workspace_id=uuid, id=uuid) - - async with aqtbot.wait_signal(w_w.mountpoint_started): - w_w.event_bus.send( - CoreEvent.MOUNTPOINT_STARTED, - mountpoint=None, - workspace_id=uuid, - timestamp=pendulum.now(), - ) - - assert not autoclose_dialog.dialogs - async with aqtbot.wait_signal(w_w.mountpoint_stopped): - w_w.event_bus.send( - CoreEvent.MOUNTPOINT_STOPPED, - mountpoint=None, - workspace_id=uuid, - timestamp=pendulum.now(), - ) - assert autoclose_dialog.dialogs == [ - ( - "Error", - "Your permissions on this workspace have been revoked. You no longer have access to theses files.", - ) - ] - - @pytest.mark.gui @pytest.mark.trio async def test_mountpoint_open_in_explorer_button(aqtbot, running_backend, logged_gui, monkeypatch): From b4eb68eebbffdff9220ae1069b0d78d5e38053bf Mon Sep 17 00:00:00 2001 From: Vincent Michel Date: Thu, 29 Jul 2021 19:19:30 +0200 Subject: [PATCH 32/35] Make success and error signals more consistent with the job argument --- parsec/core/gui/claim_user_widget.py | 4 ++-- parsec/core/gui/create_org_widget.py | 13 +++++----- parsec/core/gui/file_history_widget.py | 10 ++++---- parsec/core/gui/files_widget.py | 4 ++-- parsec/core/gui/instance_widget.py | 11 +++++---- parsec/core/gui/new_version.py | 11 +++++---- .../core/gui/timestamped_workspace_widget.py | 11 +++++---- parsec/core/gui/trio_thread.py | 24 +++++++------------ 8 files changed, 46 insertions(+), 42 deletions(-) diff --git a/parsec/core/gui/claim_user_widget.py b/parsec/core/gui/claim_user_widget.py index 5ff2af5cf4b..771f74b3b0f 100644 --- a/parsec/core/gui/claim_user_widget.py +++ b/parsec/core/gui/claim_user_widget.py @@ -299,7 +299,7 @@ def _on_get_greeter_sas_error(self, job): show_error(self, msg, exception=exc) self.failed.emit(job) - def _on_get_claimer_sas_success(self): + def _on_get_claimer_sas_success(self, job): assert self.get_claimer_sas_job assert self.get_claimer_sas_job.is_finished() assert self.get_claimer_sas_job.status == "ok" @@ -321,7 +321,7 @@ def _on_get_claimer_sas_error(self, job): assert job.status != "ok" self.failed.emit(job) - def _on_signify_trust_success(self): + def _on_signify_trust_success(self, job): assert self.signify_trust_job assert self.signify_trust_job.is_finished() assert self.signify_trust_job.status == "ok" diff --git a/parsec/core/gui/create_org_widget.py b/parsec/core/gui/create_org_widget.py index f87b7856323..9a0271e8fb0 100644 --- a/parsec/core/gui/create_org_widget.py +++ b/parsec/core/gui/create_org_widget.py @@ -21,6 +21,7 @@ ) from parsec.core.local_device import save_device_with_password +from parsec.core.gui.trio_thread import QtToTrioJob from parsec.core.gui.custom_dialogs import GreyedDialog, show_error, show_info from parsec.core.gui.trio_thread import JobResultError from parsec.core.gui.desktop import get_default_device @@ -143,8 +144,8 @@ def password(self): class CreateOrgWidget(QWidget, Ui_CreateOrgWidget): - req_success = pyqtSignal() - req_error = pyqtSignal() + req_success = pyqtSignal(QtToTrioJob) + req_error = pyqtSignal(QtToTrioJob) def __init__(self, jobs_ctx, config, start_addr): super().__init__() @@ -296,8 +297,8 @@ def _on_validate_clicked(self): ) self.button_validate.setEnabled(False) - def _on_req_success(self): - assert self.create_job + def _on_req_success(self, job): + assert self.create_job is job assert self.create_job.is_finished() assert self.create_job.status == "ok" @@ -317,8 +318,8 @@ def _on_req_success(self): else: logger.warning("Cannot close dialog when org wizard") - def _on_req_error(self): - assert self.create_job + def _on_req_error(self, job): + assert self.create_job is job assert self.create_job.is_finished() assert self.create_job.status != "ok" diff --git a/parsec/core/gui/file_history_widget.py b/parsec/core/gui/file_history_widget.py index 89bdc10b55e..0d7b6a7b4c0 100644 --- a/parsec/core/gui/file_history_widget.py +++ b/parsec/core/gui/file_history_widget.py @@ -2,6 +2,8 @@ from PyQt5.QtCore import pyqtSignal from PyQt5.QtWidgets import QWidget + +from parsec.core.gui.trio_thread import QtToTrioJob from parsec.core.gui.lang import translate as _, format_datetime from parsec.core.gui.custom_dialogs import show_error, GreyedDialog from parsec.core.gui.file_size import get_filesize @@ -45,8 +47,8 @@ def __init__(self, version, creator, name, size, src, dst, timestamp): class FileHistoryWidget(QWidget, Ui_FileHistoryWidget): - get_versions_success = pyqtSignal() - get_versions_error = pyqtSignal() + get_versions_success = pyqtSignal(QtToTrioJob) + get_versions_error = pyqtSignal(QtToTrioJob) def __init__( self, @@ -121,7 +123,7 @@ def add_history_item(self, version, path, creator, size, timestamp, src_path, ds self.layout_history.addWidget(button) button.show() - def on_get_version_success(self): + def on_get_version_success(self, job): versions_list, download_limit_reached = self.versions_job.ret if download_limit_reached: self.button_load_more_entries.setVisible(False) @@ -138,7 +140,7 @@ def on_get_version_success(self): ) self.set_loading_in_progress(False) - def on_get_version_error(self): + def on_get_version_error(self, job): if self.versions_job and self.versions_job.status != "cancelled": show_error(self, _("TEXT_FILE_HISTORY_LIST_FAILURE"), exception=self.versions_job.exc) self.versions_job = None diff --git a/parsec/core/gui/files_widget.py b/parsec/core/gui/files_widget.py index 28bc2b92d57..bc863f9ab7b 100644 --- a/parsec/core/gui/files_widget.py +++ b/parsec/core/gui/files_widget.py @@ -915,14 +915,14 @@ def _on_folder_create_error(self, job): else: show_error(self, _("TEXT_FILE_FOLDER_CREATE_ERROR_UNKNOWN")) - def _on_import_success(self): + def _on_import_success(self, job): assert self.loading_dialog self.loading_dialog.hide() self.loading_dialog.setParent(None) self.loading_dialog = None self.import_job = None - def _on_import_error(self): + def _on_import_error(self, job): def _display_import_error(file_count, exceptions=None): if exceptions and all(isinstance(exc, PermissionError) for exc in exceptions): if file_count and file_count == 1: diff --git a/parsec/core/gui/instance_widget.py b/parsec/core/gui/instance_widget.py index 09087332e50..05430b2a84a 100644 --- a/parsec/core/gui/instance_widget.py +++ b/parsec/core/gui/instance_widget.py @@ -19,6 +19,7 @@ MountpointWinfspNotAvailable, ) +from parsec.core.gui.trio_thread import QtToTrioJob from parsec.core.gui.trio_thread import QtToTrioJobScheduler, run_trio_job_scheduler from parsec.core.gui.parsec_application import ParsecApp from parsec.core.gui.custom_dialogs import show_error, show_info_link @@ -56,8 +57,8 @@ def ensure_macfuse_available_or_show_dialogue(window): class InstanceWidget(QWidget): - run_core_success = pyqtSignal() - run_core_error = pyqtSignal() + run_core_success = pyqtSignal(QtToTrioJob) + run_core_error = pyqtSignal(QtToTrioJob) run_core_ready = pyqtSignal(object, object) logged_in = pyqtSignal() logged_out = pyqtSignal() @@ -149,7 +150,8 @@ def on_run_core_ready(self, core, core_jobs_ctx): ) self.logged_in.emit() - def on_core_run_error(self): + def on_core_run_error(self, job): + assert job is self.running_core_job assert self.running_core_job.is_finished() if self.core: self.core.event_bus.disconnect( @@ -178,7 +180,8 @@ def on_core_run_error(self): self.running_core_job = None self.logged_out.emit() - def on_core_run_done(self): + def on_core_run_done(self, job): + assert job is self.running_core_job assert self.running_core_job.is_finished() if self.core: ParsecApp.remove_connected_device( diff --git a/parsec/core/gui/new_version.py b/parsec/core/gui/new_version.py index 25c43c58f41..201d8d027fd 100644 --- a/parsec/core/gui/new_version.py +++ b/parsec/core/gui/new_version.py @@ -17,6 +17,7 @@ from parsec.serde import BaseSchema, fields, JSONSerializer, SerdeError from parsec.core.gui import desktop from parsec.core.gui.lang import translate as _ +from parsec.core.gui.trio_thread import QtToTrioJob from parsec.core.gui.ui.new_version_dialog import Ui_NewVersionDialog from parsec.core.gui.ui.new_version_info import Ui_NewVersionInfo from parsec.core.gui.ui.new_version_available import Ui_NewVersionAvailable @@ -175,8 +176,8 @@ def set_version(self, version): class CheckNewVersion(QDialog, Ui_NewVersionDialog): - check_new_version_success = pyqtSignal() - check_new_version_error = pyqtSignal() + check_new_version_success = pyqtSignal(QtToTrioJob) + check_new_version_error = pyqtSignal(QtToTrioJob) def __init__(self, jobs_ctx, event_bus, config, **kwargs): super().__init__(**kwargs) @@ -211,7 +212,8 @@ def __init__(self, jobs_ctx, event_bus, config, **kwargs): ) self.setWindowFlags(Qt.SplashScreen) - def on_check_new_version_success(self): + def on_check_new_version_success(self, job): + assert job is self.version_job assert self.version_job.is_finished() assert self.version_job.status == "ok" version_job_ret = self.version_job.ret @@ -231,7 +233,8 @@ def on_check_new_version_success(self): self.widget_info.show() self.widget_info.show_up_to_date() - def on_check_new_version_error(self): + def on_check_new_version_error(self, job): + assert job is self.version_job assert self.version_job.is_finished() assert self.version_job.status != "ok" self.version_job = None diff --git a/parsec/core/gui/timestamped_workspace_widget.py b/parsec/core/gui/timestamped_workspace_widget.py index de37f86abe9..1848978e17b 100644 --- a/parsec/core/gui/timestamped_workspace_widget.py +++ b/parsec/core/gui/timestamped_workspace_widget.py @@ -8,6 +8,7 @@ import pendulum +from parsec.core.gui.trio_thread import QtToTrioJob from parsec.core.gui.lang import get_qlocale, translate as _, format_datetime from parsec.core.gui.custom_dialogs import show_error, GreyedDialog from parsec.core.gui.ui.timestamped_workspace_widget import Ui_TimestampedWorkspaceWidget @@ -22,8 +23,8 @@ async def _do_workspace_get_creation_timestamp(workspace_fs): class TimestampedWorkspaceWidget(QWidget, Ui_TimestampedWorkspaceWidget): - get_creation_timestamp_success = pyqtSignal() - get_creation_timestamp_error = pyqtSignal() + get_creation_timestamp_success = pyqtSignal(QtToTrioJob) + get_creation_timestamp_error = pyqtSignal(QtToTrioJob) def __init__(self, workspace_fs, jobs_ctx): super().__init__() @@ -36,7 +37,7 @@ def __init__(self, workspace_fs, jobs_ctx): fmt = self.calendar_widget.weekdayTextFormat(d) fmt.setForeground(QColor(0, 0, 0)) self.calendar_widget.setWeekdayTextFormat(d, fmt) - self.get_creation_timestamp_success.connect(self.enable_with_timestamp) + self.get_creation_timestamp_success.connect(self.on_success) self.get_creation_timestamp_error.connect(self.on_error) self.limits_job = self.jobs_ctx.submit_job( self.get_creation_timestamp_success, @@ -77,7 +78,7 @@ def set_time_limits(self): else: self.time_edit.clearMaximumTime() - def on_error(self): + def on_error(self, job): if self.limits_job and self.limits_job.status != "cancelled": show_error( self, @@ -87,7 +88,7 @@ def on_error(self): self.limits_job = None self.dialog.reject() - def enable_with_timestamp(self): + def on_success(self, job): creation = self.limits_job.ret.in_timezone("local") self.limits_job = None self.creation_date = (creation.year, creation.month, creation.day) diff --git a/parsec/core/gui/trio_thread.py b/parsec/core/gui/trio_thread.py index c9121cba976..85159abfaac 100644 --- a/parsec/core/gui/trio_thread.py +++ b/parsec/core/gui/trio_thread.py @@ -26,9 +26,9 @@ class JobSchedulerNotAvailable(Exception): class QtToTrioJob: - def __init__(self, fn, args, kwargs, qt_on_success, qt_on_error): - self._qt_on_success = qt_on_success - self._qt_on_error = qt_on_error + def __init__(self, fn, args, kwargs, on_success, on_error): + self._on_success = on_success + self._on_error = on_error self._fn = fn self._args = args self._kwargs = kwargs @@ -119,14 +119,8 @@ def set_exception(self, exc): def _set_done(self): self._done.set() - signal = self._qt_on_success if self.is_ok() else self._qt_on_error - # TODO: Either pick a consistent API for those signals or get rid of the altogether. - # In the meantime, hack into the signal time in order to detect whether the job should - # be emitted as an argument to the signal - if signal.signal.endswith("(PyQt_PyObject)"): - signal.emit(self) - else: - signal.emit() + signal = self._on_success if self.is_ok() else self._on_error + signal.emit(self) def cancel(self): self.cancel_scope.cancel() @@ -142,7 +136,7 @@ def close(self): self.nursery.cancel_scope.cancel() def submit_throttled_job( - self, throttling_id: str, delay: float, qt_on_success, qt_on_error, fn, *args, **kwargs + self, throttling_id: str, delay: float, on_success, on_error, fn, *args, **kwargs ): """ Throttle execution: immediatly execute `fn` unless a job with a similar @@ -183,15 +177,15 @@ async def _throttled_execute(job, task_status=trio.TASK_STATUS_IGNORED): pass # Create the job but don't execute it: we have to handle throttle first ! - job = QtToTrioJob(fn, args, kwargs, qt_on_success, qt_on_error) + job = QtToTrioJob(fn, args, kwargs, on_success, on_error) if self.nursery._closed: job.set_cancelled(JobSchedulerNotAvailable()) else: self.nursery.start_soon(_throttled_execute, job) return job - def submit_job(self, qt_on_success, qt_on_error, fn, *args, **kwargs): - job = QtToTrioJob(fn, args, kwargs, qt_on_success, qt_on_error) + def submit_job(self, on_success, on_error, fn, *args, **kwargs): + job = QtToTrioJob(fn, args, kwargs, on_success, on_error) if self.nursery._closed: job.set_cancelled(JobSchedulerNotAvailable()) else: From f84dd072a2e37ba054ba7df529b8d4add06e6237 Mon Sep 17 00:00:00 2001 From: Vincent Michel Date: Fri, 30 Jul 2021 16:28:18 +0200 Subject: [PATCH 33/35] Set the current directory directly in the FilesWidget.load method --- parsec/core/gui/files_widget.py | 73 +++++++++++++++++++++------------ 1 file changed, 47 insertions(+), 26 deletions(-) diff --git a/parsec/core/gui/files_widget.py b/parsec/core/gui/files_widget.py index bc863f9ab7b..a92468325ac 100644 --- a/parsec/core/gui/files_widget.py +++ b/parsec/core/gui/files_widget.py @@ -1,6 +1,8 @@ # Parsec Cloud (https://parsec.cloud) Copyright (c) AGPLv3 2016-2021 Scille SAS import trio import pathlib +from uuid import UUID +from typing import Optional from parsec.core.core_events import CoreEvent from pendulum import DateTime @@ -152,7 +154,7 @@ async def _do_move_files(workspace_fs, target_dir, source_files, source_workspac raise JobResultError("error", last_exc=last_exc, error_count=error_count) -async def _do_folder_stat(workspace_fs, path, default_selection, set_path): +async def _do_folder_stat(workspace_fs, path, default_selection): stats = {} dir_stat = await workspace_fs.path_info(path) # Retrieve children info, this is not an atomic operation so our view @@ -172,7 +174,7 @@ async def _do_folder_stat(workspace_fs, path, default_selection, set_path): # if the manifest is inconsistent (broken data or signature). child_stat = {"type": "inconsistency", "id": exc.args[0]} stats[child] = child_stat - return path, dir_stat["id"], stats, default_selection, set_path + return path, dir_stat["id"], stats, default_selection async def _do_folder_create(workspace_fs, path): @@ -305,8 +307,15 @@ def __init__(self, core, jobs_ctx, event_bus, *args, **kwargs): self.button_create_folder.apply_style() self.line_edit_search.textChanged.connect(self.filter_files) self.line_edit_search.hide() - self.current_directory = FsPath("/") - self.current_directory_uuid = None + + # Current directory UUID can be `None`. + # This means that the corresponding UUID is still unkown since the + # folder stat job has not returned the info yet. It could also be that + # the job has failed to return properly, maybe because the directory + # no longer exists + self.current_directory: FsPath = FsPath("/") + self.current_directory_uuid: Optional[UUID] = None + self.default_import_path = str(pathlib.Path.home()) self.table_files.config = self.core.config self.table_files.file_moved.connect(self.on_table_files_file_moved) @@ -603,9 +612,6 @@ def open_files(self): show_error(self, _("TEXT_FILE_OPEN_MULTIPLE_ERROR")) def open_file(self, file_name): - # The Qt thread should never hit the core directly. - # Synchronous calls can run directly in the job system - # as they won't block the Qt loop for long path = self.core.mountpoint_manager.get_path_in_mountpoint( self.workspace_fs.workspace_id, self.current_directory / file_name if file_name else self.current_directory, @@ -636,11 +642,12 @@ def reload(self, default_selection=None, delay=RELOAD_FILES_LIST_THROTTLE_DELAY) workspace_fs=self.workspace_fs, path=self.current_directory, default_selection=default_selection, - set_path=False, ) def load(self, directory, default_selection=None): self.spinner.show() + self.current_directory = directory + self.current_directory_uuid = None self.jobs_ctx.submit_job( self.folder_stat_success, self.folder_stat_error, @@ -648,7 +655,6 @@ def load(self, directory, default_selection=None): workspace_fs=self.workspace_fs, path=directory, default_selection=default_selection, - set_path=True, ) def import_all(self, files, total_size): @@ -831,22 +837,18 @@ def _on_delete_error(self, job): def _on_folder_stat_success(self, job): # Extract job information - directory, directory_uuid, files_stats, default_selection, set_path = job.ret + directory, directory_uuid, files_stats, default_selection = job.ret # Ignore old refresh jobs - if not set_path and self.current_directory != directory: + if self.current_directory != directory: return - # Set the current directory - old_current_directory = self.current_directory - self.current_directory, self.current_directory_uuid = directory, directory_uuid - # Trigger a refresh to avoid race conditions - # TODO: maybe find a better way to deal with race conditions - if set_path: - self.reload() + # Set the UUID + first_refresh = self.current_directory_uuid is None + self.current_directory_uuid = directory_uuid # Try to keep the current selection - if old_current_directory == self.current_directory: - old_selection = [x.name for x in self.table_files.selected_files()] - else: + if first_refresh: old_selection = set() + else: + old_selection = [x.name for x in self.table_files.selected_files()] self.table_files.clear() self.spinner.hide() @@ -971,13 +973,20 @@ def _display_import_error(file_count, exceptions=None): self.import_job = None def _on_fs_entry_downsynced(self, event, workspace_id=None, id=None): + # No workspace FS if not self.workspace_fs: return - ws_id = self.workspace_fs.workspace_id - if ws_id != workspace_id: + # Not the corresponding workspace + if workspace_id != self.workspace_fs.workspace_id: return - if id == self.current_directory_uuid: + # Reload, as it might correspond to the current directory + if self.current_directory_uuid is None: self.reload() + return + # Reload, as it definitely corresponds to the current directory + if self.current_directory_uuid == id: + self.reload() + return def _on_fs_entry_synced(self, event, id, workspace_id=None): if not self.workspace_fs: @@ -998,12 +1007,24 @@ def _on_fs_entry_synced(self, event, id, workspace_id=None): def _on_fs_entry_updated(self, event, workspace_id=None, id=None): assert id is not None - if self.workspace_fs is None: + # No workspace FS + if not self.workspace_fs: return + # Not the corresponding workspace if workspace_id != self.workspace_fs.workspace_id: return - if self.current_directory_uuid == id or self.table_files.has_file(id): + # Reload, as it might correspond to the current directory + if self.current_directory_uuid is None: + self.reload() + return + # Reload, as it definitely corresponds to the current directory + if self.current_directory_uuid == id: self.reload() + return + # Reload, as the id appears in the table files + if self.table_files.has_file(id): + self.reload() + return def _on_sharing_updated(self, event, new_entry, previous_entry): if new_entry is None or new_entry.role is None: From 3a1f1acb420060bf7330a27ee556f64fd7c96756 Mon Sep 17 00:00:00 2001 From: Vincent Michel Date: Fri, 30 Jul 2021 16:31:30 +0200 Subject: [PATCH 34/35] Bump qtrio and attrs --- setup.py | 4 ++-- tests/core/fs/workspacefs/test_workspace_fs.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/setup.py b/setup.py index 91c21fb35f9..bbb81c1534d 100644 --- a/setup.py +++ b/setup.py @@ -272,7 +272,7 @@ def run(self): requirements = [ - "attrs==19.2.0", + "attrs==21.2.0", "click==7.1.2", "msgpack==0.6.0", "wsproto==1.0.0", @@ -318,7 +318,7 @@ def run(self): ] -PYQT_DEPS = ["PyQt5==5.15.2", "pyqt5-sip==12.8.1", "qtrio==0.4.2"] +PYQT_DEPS = ["PyQt5==5.15.2", "pyqt5-sip==12.8.1", "qtrio==0.5.0"] GUI_DEPS = [*PYQT_DEPS, "qrcode==6.1"] BABEL_DEP = "Babel==2.6.0" WHEEL_DEP = "wheel==0.34.2" diff --git a/tests/core/fs/workspacefs/test_workspace_fs.py b/tests/core/fs/workspacefs/test_workspace_fs.py index 37f2bc7673f..2f5f9699550 100644 --- a/tests/core/fs/workspacefs/test_workspace_fs.py +++ b/tests/core/fs/workspacefs/test_workspace_fs.py @@ -367,8 +367,8 @@ async def test_dump(alice_workspace): "need_sync": False, "parent": ANY, "updated": ANY, - "local_confinement_points": frozenset(), - "remote_confinement_points": frozenset(), + "local_confinement_points": [], + "remote_confinement_points": [], } }, "created": ANY, @@ -376,8 +376,8 @@ async def test_dump(alice_workspace): "is_placeholder": False, "need_sync": False, "updated": ANY, - "local_confinement_points": frozenset(), - "remote_confinement_points": frozenset(), + "local_confinement_points": [], + "remote_confinement_points": [], } From c3302fc97a42d801d8095bc5777bb8bcce2e547e Mon Sep 17 00:00:00 2001 From: Vincent Michel Date: Mon, 2 Aug 2021 16:51:52 +0200 Subject: [PATCH 35/35] Address the last bunch of @touilleMan's comments --- parsec/core/gui/app.py | 2 +- parsec/core/gui/central_widget.py | 4 ++-- parsec/core/gui/claim_device_widget.py | 2 +- parsec/core/gui/claim_user_widget.py | 2 +- parsec/core/gui/create_org_widget.py | 4 ++-- parsec/core/gui/devices_widget.py | 2 +- parsec/core/gui/file_history_widget.py | 2 +- parsec/core/gui/files_widget.py | 3 ++- parsec/core/gui/greet_device_widget.py | 2 +- parsec/core/gui/greet_user_widget.py | 2 +- parsec/core/gui/instance_widget.py | 4 ++-- parsec/core/gui/main_window.py | 2 +- parsec/core/gui/new_version.py | 2 +- parsec/core/gui/timestamped_workspace_widget.py | 6 ++++-- parsec/core/gui/{trio_thread.py => trio_jobs.py} | 0 parsec/core/gui/users_widget.py | 2 +- parsec/core/gui/workspace_sharing_widget.py | 2 +- parsec/core/gui/workspaces_widget.py | 2 +- parsec/core/mountpoint/manager.py | 7 ++++++- tests/core/gui/conftest.py | 4 ++-- tests/core/gui/test_workspaces_reencrypt.py | 2 +- 21 files changed, 33 insertions(+), 25 deletions(-) rename parsec/core/gui/{trio_thread.py => trio_jobs.py} (100%) diff --git a/parsec/core/gui/app.py b/parsec/core/gui/app.py index 9d7e3367dfd..8016c93a6f1 100644 --- a/parsec/core/gui/app.py +++ b/parsec/core/gui/app.py @@ -28,7 +28,7 @@ from parsec.core.gui.new_version import CheckNewVersion from parsec.core.gui.systray import systray_available, Systray from parsec.core.gui.main_window import MainWindow - from parsec.core.gui.trio_thread import run_trio_job_scheduler + from parsec.core.gui.trio_jobs import run_trio_job_scheduler except ImportError as exc: raise ModuleNotFoundError( """PyQt forms haven't been generated. diff --git a/parsec/core/gui/central_widget.py b/parsec/core/gui/central_widget.py index e08227e7b65..5a4b10002ec 100644 --- a/parsec/core/gui/central_widget.py +++ b/parsec/core/gui/central_widget.py @@ -27,7 +27,7 @@ FSWorkspaceNoWriteAccess, FSWorkspaceInMaintenance, ) -from parsec.core.gui.trio_thread import QtToTrioJobScheduler +from parsec.core.gui.trio_jobs import QtToTrioJobScheduler from parsec.core.gui.mount_widget import MountWidget from parsec.core.gui.users_widget import UsersWidget from parsec.core.gui.devices_widget import DevicesWidget @@ -37,7 +37,7 @@ from parsec.core.gui.custom_widgets import Pixmap from parsec.core.gui.custom_dialogs import show_error from parsec.core.gui.ui.central_widget import Ui_CentralWidget -from parsec.core.gui.trio_thread import JobResultError, QtToTrioJob +from parsec.core.gui.trio_jobs import JobResultError, QtToTrioJob async def _do_get_organization_stats(core: LoggedCore) -> OrganizationStats: diff --git a/parsec/core/gui/claim_device_widget.py b/parsec/core/gui/claim_device_widget.py index d0e1a6b860e..21112a52b3c 100644 --- a/parsec/core/gui/claim_device_widget.py +++ b/parsec/core/gui/claim_device_widget.py @@ -17,7 +17,7 @@ BackendNotAvailable, ) from parsec.core.gui import validators -from parsec.core.gui.trio_thread import JobResultError, QtToTrioJob +from parsec.core.gui.trio_jobs import JobResultError, QtToTrioJob from parsec.core.gui.desktop import get_default_device from parsec.core.gui.custom_dialogs import show_error, GreyedDialog, show_info from parsec.core.gui.lang import translate as _ diff --git a/parsec/core/gui/claim_user_widget.py b/parsec/core/gui/claim_user_widget.py index 771f74b3b0f..494ef74d2f2 100644 --- a/parsec/core/gui/claim_user_widget.py +++ b/parsec/core/gui/claim_user_widget.py @@ -18,7 +18,7 @@ BackendNotAvailable, ) from parsec.core.gui import validators -from parsec.core.gui.trio_thread import JobResultError, QtToTrioJob +from parsec.core.gui.trio_jobs import JobResultError, QtToTrioJob from parsec.core.gui.desktop import get_default_device from parsec.core.gui.custom_dialogs import show_error, GreyedDialog, show_info from parsec.core.gui.lang import translate as _ diff --git a/parsec/core/gui/create_org_widget.py b/parsec/core/gui/create_org_widget.py index 9a0271e8fb0..a403a9c1ac5 100644 --- a/parsec/core/gui/create_org_widget.py +++ b/parsec/core/gui/create_org_widget.py @@ -21,9 +21,9 @@ ) from parsec.core.local_device import save_device_with_password -from parsec.core.gui.trio_thread import QtToTrioJob +from parsec.core.gui.trio_jobs import QtToTrioJob from parsec.core.gui.custom_dialogs import GreyedDialog, show_error, show_info -from parsec.core.gui.trio_thread import JobResultError +from parsec.core.gui.trio_jobs import JobResultError from parsec.core.gui.desktop import get_default_device from parsec.core.gui.lang import translate as _ from parsec.core.gui import validators diff --git a/parsec/core/gui/devices_widget.py b/parsec/core/gui/devices_widget.py index a13a455f8aa..032a5bc50d2 100644 --- a/parsec/core/gui/devices_widget.py +++ b/parsec/core/gui/devices_widget.py @@ -5,7 +5,7 @@ from PyQt5.QtGui import QColor from parsec.core.backend_connection import BackendNotAvailable, BackendConnectionError -from parsec.core.gui.trio_thread import JobResultError, QtToTrioJob +from parsec.core.gui.trio_jobs import JobResultError, QtToTrioJob from parsec.core.gui.greet_device_widget import GreetDeviceWidget from parsec.core.gui.lang import translate as _ from parsec.core.gui.custom_widgets import ensure_string_size diff --git a/parsec/core/gui/file_history_widget.py b/parsec/core/gui/file_history_widget.py index 0d7b6a7b4c0..8600cfb770e 100644 --- a/parsec/core/gui/file_history_widget.py +++ b/parsec/core/gui/file_history_widget.py @@ -3,7 +3,7 @@ from PyQt5.QtCore import pyqtSignal from PyQt5.QtWidgets import QWidget -from parsec.core.gui.trio_thread import QtToTrioJob +from parsec.core.gui.trio_jobs import QtToTrioJob from parsec.core.gui.lang import translate as _, format_datetime from parsec.core.gui.custom_dialogs import show_error, GreyedDialog from parsec.core.gui.file_size import get_filesize diff --git a/parsec/core/gui/files_widget.py b/parsec/core/gui/files_widget.py index a92468325ac..553cf47c3ba 100644 --- a/parsec/core/gui/files_widget.py +++ b/parsec/core/gui/files_widget.py @@ -19,7 +19,7 @@ FSFileNotFoundError, ) -from parsec.core.gui.trio_thread import JobResultError, QtToTrioJob +from parsec.core.gui.trio_jobs import JobResultError, QtToTrioJob from parsec.core.gui import desktop from parsec.core.gui.file_items import FileType, TYPE_DATA_INDEX, UUID_DATA_INDEX from parsec.core.gui.custom_dialogs import ( @@ -918,6 +918,7 @@ def _on_folder_create_error(self, job): show_error(self, _("TEXT_FILE_FOLDER_CREATE_ERROR_UNKNOWN")) def _on_import_success(self, job): + assert self.import_job is job assert self.loading_dialog self.loading_dialog.hide() self.loading_dialog.setParent(None) diff --git a/parsec/core/gui/greet_device_widget.py b/parsec/core/gui/greet_device_widget.py index c74e0bae03b..d38ec78bc3f 100644 --- a/parsec/core/gui/greet_device_widget.py +++ b/parsec/core/gui/greet_device_widget.py @@ -8,7 +8,7 @@ from parsec.core.backend_connection import BackendNotAvailable, BackendConnectionError from parsec.core.invite import InviteError, InvitePeerResetError, InviteAlreadyUsedError -from parsec.core.gui.trio_thread import JobResultError, QtToTrioJob +from parsec.core.gui.trio_jobs import JobResultError, QtToTrioJob from parsec.core.gui.custom_dialogs import show_error, GreyedDialog, show_info from parsec.core.gui.lang import translate as _ from parsec.core.gui.qrcode_widget import generate_qr_code diff --git a/parsec/core/gui/greet_user_widget.py b/parsec/core/gui/greet_user_widget.py index 5d09a70e245..b75ac7a54f4 100644 --- a/parsec/core/gui/greet_user_widget.py +++ b/parsec/core/gui/greet_user_widget.py @@ -10,7 +10,7 @@ from parsec.api.protocol import HumanHandle from parsec.core.backend_connection import BackendNotAvailable from parsec.core.invite import InviteError, InvitePeerResetError, InviteAlreadyUsedError -from parsec.core.gui.trio_thread import JobResultError, QtToTrioJob +from parsec.core.gui.trio_jobs import JobResultError, QtToTrioJob from parsec.core.gui.custom_dialogs import show_error, GreyedDialog, show_info from parsec.core.gui import validators from parsec.core.gui.lang import translate as _ diff --git a/parsec/core/gui/instance_widget.py b/parsec/core/gui/instance_widget.py index 05430b2a84a..1c1cdee01ac 100644 --- a/parsec/core/gui/instance_widget.py +++ b/parsec/core/gui/instance_widget.py @@ -19,8 +19,8 @@ MountpointWinfspNotAvailable, ) -from parsec.core.gui.trio_thread import QtToTrioJob -from parsec.core.gui.trio_thread import QtToTrioJobScheduler, run_trio_job_scheduler +from parsec.core.gui.trio_jobs import QtToTrioJob +from parsec.core.gui.trio_jobs import QtToTrioJobScheduler, run_trio_job_scheduler from parsec.core.gui.parsec_application import ParsecApp from parsec.core.gui.custom_dialogs import show_error, show_info_link from parsec.core.gui.lang import translate as _ diff --git a/parsec/core/gui/main_window.py b/parsec/core/gui/main_window.py index 0e9ec2b95cb..a8d18609a45 100644 --- a/parsec/core/gui/main_window.py +++ b/parsec/core/gui/main_window.py @@ -22,7 +22,7 @@ BackendOrganizationFileLinkAddr, ) from parsec.api.protocol import InvitationType -from parsec.core.gui.trio_thread import QtToTrioJobScheduler +from parsec.core.gui.trio_jobs import QtToTrioJobScheduler from parsec.core.gui.lang import translate as _ from parsec.core.gui.instance_widget import InstanceWidget from parsec.core.gui.parsec_application import ParsecApp diff --git a/parsec/core/gui/new_version.py b/parsec/core/gui/new_version.py index 201d8d027fd..72538149d46 100644 --- a/parsec/core/gui/new_version.py +++ b/parsec/core/gui/new_version.py @@ -17,7 +17,7 @@ from parsec.serde import BaseSchema, fields, JSONSerializer, SerdeError from parsec.core.gui import desktop from parsec.core.gui.lang import translate as _ -from parsec.core.gui.trio_thread import QtToTrioJob +from parsec.core.gui.trio_jobs import QtToTrioJob from parsec.core.gui.ui.new_version_dialog import Ui_NewVersionDialog from parsec.core.gui.ui.new_version_info import Ui_NewVersionInfo from parsec.core.gui.ui.new_version_available import Ui_NewVersionAvailable diff --git a/parsec/core/gui/timestamped_workspace_widget.py b/parsec/core/gui/timestamped_workspace_widget.py index 1848978e17b..4c960ab8d3f 100644 --- a/parsec/core/gui/timestamped_workspace_widget.py +++ b/parsec/core/gui/timestamped_workspace_widget.py @@ -8,7 +8,7 @@ import pendulum -from parsec.core.gui.trio_thread import QtToTrioJob +from parsec.core.gui.trio_jobs import QtToTrioJob from parsec.core.gui.lang import get_qlocale, translate as _, format_datetime from parsec.core.gui.custom_dialogs import show_error, GreyedDialog from parsec.core.gui.ui.timestamped_workspace_widget import Ui_TimestampedWorkspaceWidget @@ -79,7 +79,8 @@ def set_time_limits(self): self.time_edit.clearMaximumTime() def on_error(self, job): - if self.limits_job and self.limits_job.status != "cancelled": + assert self.limits_job is job + if self.limits_job.status != "cancelled": show_error( self, _("TEXT_WORKSPACE_TIMESTAMPED_VERSION_RETRIEVAL_FAILED"), @@ -89,6 +90,7 @@ def on_error(self, job): self.dialog.reject() def on_success(self, job): + assert self.limits_job is job creation = self.limits_job.ret.in_timezone("local") self.limits_job = None self.creation_date = (creation.year, creation.month, creation.day) diff --git a/parsec/core/gui/trio_thread.py b/parsec/core/gui/trio_jobs.py similarity index 100% rename from parsec/core/gui/trio_thread.py rename to parsec/core/gui/trio_jobs.py diff --git a/parsec/core/gui/users_widget.py b/parsec/core/gui/users_widget.py index 91185a52f54..14433c41b84 100644 --- a/parsec/core/gui/users_widget.py +++ b/parsec/core/gui/users_widget.py @@ -18,7 +18,7 @@ BackendInvitationOnExistingMember, ) -from parsec.core.gui.trio_thread import JobResultError, QtToTrioJob +from parsec.core.gui.trio_jobs import JobResultError, QtToTrioJob from parsec.core.gui.custom_dialogs import show_error, show_info, ask_question, get_text_input from parsec.core.gui.custom_widgets import ensure_string_size from parsec.core.gui.flow_layout import FlowLayout diff --git a/parsec/core/gui/workspace_sharing_widget.py b/parsec/core/gui/workspace_sharing_widget.py index dd8a115a645..b985f2b6c5b 100644 --- a/parsec/core/gui/workspace_sharing_widget.py +++ b/parsec/core/gui/workspace_sharing_widget.py @@ -11,7 +11,7 @@ from parsec.core.types import WorkspaceRole from parsec.core.backend_connection import BackendNotAvailable -from parsec.core.gui.trio_thread import JobResultError, QtToTrioJob +from parsec.core.gui.trio_jobs import JobResultError, QtToTrioJob from parsec.core.gui.custom_dialogs import show_error, GreyedDialog from parsec.core.gui.custom_widgets import Pixmap diff --git a/parsec/core/gui/workspaces_widget.py b/parsec/core/gui/workspaces_widget.py index 70a94e95fd2..4f24021196b 100644 --- a/parsec/core/gui/workspaces_widget.py +++ b/parsec/core/gui/workspaces_widget.py @@ -34,7 +34,7 @@ MountpointNoDriveAvailable, ) -from parsec.core.gui.trio_thread import JobResultError, QtToTrioJob, JobSchedulerNotAvailable +from parsec.core.gui.trio_jobs import JobResultError, QtToTrioJob, JobSchedulerNotAvailable from parsec.core.gui import desktop from parsec.core.gui.custom_dialogs import show_error, get_text_input, ask_question from parsec.core.gui.flow_layout import FlowLayout diff --git a/parsec/core/mountpoint/manager.py b/parsec/core/mountpoint/manager.py index 9c3a62d930b..355ab9fcfc4 100644 --- a/parsec/core/mountpoint/manager.py +++ b/parsec/core/mountpoint/manager.py @@ -1,6 +1,5 @@ # Parsec Cloud (https://parsec.cloud) Copyright (c) AGPLv3 2016-2021 Scille SAS -from parsec.core.core_events import CoreEvent import trio import logging import sys @@ -14,6 +13,7 @@ from async_generator import asynccontextmanager +from parsec.core.core_events import CoreEvent from parsec.core.types import FsPath, EntryID from parsec.core.fs.workspacefs import WorkspaceFSTimestamped from parsec.utils import TaskStatus, start_task, open_service_nursery @@ -175,6 +175,11 @@ async def curried_runner(task_status: TaskStatus): self._mountpoint_tasks[key] = task_status self.event_bus.send(CoreEvent.MOUNTPOINT_STARTED, **event_kwargs) + # It is the reponsability of the runner context teardown to wait + # for cancellation. This is done to avoid adding an extra nursery + # into the winfsp runner, for simplicity. This could change in the + # future in which case we'll simmply add a `sleep_forever` below. + finally: # Pop the mountpoint task if its ours if self._mountpoint_tasks.get(key) == task_status: diff --git a/tests/core/gui/conftest.py b/tests/core/gui/conftest.py index 1b67e0e8e2c..8335b0d299e 100644 --- a/tests/core/gui/conftest.py +++ b/tests/core/gui/conftest.py @@ -14,7 +14,7 @@ from parsec.core.local_device import save_device_with_password from parsec.core.gui.main_window import MainWindow from parsec.core.gui.workspaces_widget import WorkspaceButton -from parsec.core.gui.trio_thread import QtToTrioJobScheduler +from parsec.core.gui.trio_jobs import QtToTrioJobScheduler from parsec.core.gui.login_widget import LoginWidget, LoginPasswordInputWidget, LoginAccountsWidget from parsec.core.gui.central_widget import CentralWidget from parsec.core.gui.lang import switch_language @@ -76,7 +76,7 @@ def timed_out(): return if timed_out(): raise TimeoutError(timeout_msg) - await self.wait(10) + await trio.sleep(0.010) @asynccontextmanager async def wait_signals(self, signals, *, timeout=5000): diff --git a/tests/core/gui/test_workspaces_reencrypt.py b/tests/core/gui/test_workspaces_reencrypt.py index 970ad32f213..05614029165 100644 --- a/tests/core/gui/test_workspaces_reencrypt.py +++ b/tests/core/gui/test_workspaces_reencrypt.py @@ -336,7 +336,7 @@ def _assert_error(): # Unexpected error is logged if error_type is Exception: caplog.assert_occured( - "[exception] Uncatched error [parsec.core.gui.trio_thread]" + "[exception] Uncatched error [parsec.core.gui.trio_jobs]" )