-
Notifications
You must be signed in to change notification settings - Fork 55
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Do Not Merge] New GUI Test Tools #447
base: main
Are you sure you want to change the base?
Changes from all commits
04a404b
cad3184
f40380c
799f2ac
8b0d632
e53c5aa
2a22f4e
2114c2e
264a977
e4e3ccd
1f4516e
f99baf4
558f226
ed0db98
effe7c1
65ce9dd
e27de46
6d23d86
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
# (C) Copyright 2019 Enthought, Inc., Austin, TX | ||
# All right reserved. | ||
# | ||
# This software is provided without warranty under the terms of the BSD | ||
# license included in enthought/LICENSE.txt and may be redistributed only | ||
# under the conditions described in the aforementioned license. The license | ||
# is also available online at http://www.enthought.com/licenses/BSD.txt | ||
# Thanks for using Enthought open source! | ||
|
||
|
||
import contextlib | ||
|
||
from traits.api import HasStrictTraits, Instance | ||
|
||
from pyface.gui import GUI | ||
from pyface.i_gui import IGUI | ||
from pyface.timer.api import CallbackTimer, EventTimer | ||
|
||
|
||
class ConditionTimeoutError(RuntimeError): | ||
pass | ||
|
||
|
||
class EventLoopHelper(HasStrictTraits): | ||
""" Toolkit-independent methods for running event loops in tests. | ||
""" | ||
|
||
#: A reference to the GUI object | ||
gui = Instance(IGUI, factory=GUI) | ||
|
||
@contextlib.contextmanager | ||
def dont_quit_when_last_window_closed(self): | ||
""" Suppress exit of the application when the last window is closed. | ||
""" | ||
flag = self.gui.quit_on_last_window_close | ||
self.gui.quit_on_last_window_close = False | ||
try: | ||
yield | ||
finally: | ||
self.gui.quit_on_last_window_close = flag | ||
|
||
def event_loop(self, repeat=1, allow_user_events=True): | ||
""" Emulate an event loop running ``repeat`` times. | ||
|
||
Parameters | ||
---------- | ||
repeat : positive int | ||
The number of times to call process events. Default is 1. | ||
allow_user_events : bool | ||
Whether to process user-generated events. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Perhaps add "for example keyboard and mouse events" to clarify what's meant by "user-generated events" in this context? |
||
""" | ||
for i in range(repeat): | ||
self.gui.process_events(allow_user_events) | ||
|
||
def event_loop_until_condition(self, condition, timeout=10.0): | ||
""" Run the event loop until condition returns true, or timeout. | ||
|
||
This runs the real event loop, rather than emulating it with | ||
:meth:`GUI.process_events`. Conditions and timeouts are tracked | ||
using timers. | ||
|
||
Parameters | ||
---------- | ||
condition : callable | ||
A callable to determine if the stop criteria have been met. This | ||
should accept no arguments. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this should also be explicit about the expected return type of the callable. |
||
|
||
timeout : float | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this also be documented as "keyword only", to match |
||
Number of seconds to run the event loop in the case that the trait | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Copypasta here: "the trait change does not occur". |
||
change does not occur. | ||
|
||
Raises | ||
------ | ||
ConditionTimeoutError | ||
If the timeout occurs before the condition is True. | ||
""" | ||
|
||
with self.dont_quit_when_last_window_closed(): | ||
condition_timer = CallbackTimer.timer( | ||
stop_condition=condition, | ||
interval=0.05, | ||
expire=timeout, | ||
) | ||
condition_timer.on_trait_change(self._on_stop, 'active') | ||
|
||
try: | ||
self.gui.start_event_loop() | ||
if not condition(): | ||
raise ConditionTimeoutError( | ||
'Timed out waiting for condition') | ||
finally: | ||
condition_timer.on_trait_change( | ||
self._on_stop, 'active', remove=True) | ||
condition_timer.stop() | ||
|
||
def event_loop_with_timeout(self, repeat=2, timeout=10): | ||
""" Run the event loop for timeout seconds. | ||
|
||
Parameters | ||
---------- | ||
timeout: float, optional, keyword only | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "keyword only" doesn't seem to actually be true here. Presumably the intent is to add that extra "*" to the signature after Python 2 support has been dropped? (If so, should there be an issue open for that?) |
||
Number of seconds to run the event loop. Default value is 10.0. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should also document the conditions under which I'm not really sure I understand when this method would be used, and when it should be considered that an error has occurred. |
||
""" | ||
with self.dont_quit_when_last_window_closed(): | ||
repeat_timer = EventTimer.timer( | ||
repeat=repeat, | ||
interval=0.05, | ||
expire=timeout, | ||
) | ||
repeat_timer.on_trait_change(self._on_stop, 'active') | ||
|
||
try: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there a way to engineer a test for the corner case where the timer timeout expires before the event loop is started? (Perhaps by patching What happens if the timeout is zero? Is that a use-case we want to support, or should the docstring specify that timeout should be positive? |
||
self.gui.start_event_loop() | ||
if repeat_timer.repeat > 0: | ||
msg = 'Timed out waiting for repetition, {} remaining' | ||
raise ConditionTimeoutError( | ||
msg.format(repeat_timer.repeat) | ||
) | ||
finally: | ||
repeat_timer.on_trait_change( | ||
self._on_stop, 'active', remove=True) | ||
repeat_timer.stop() | ||
|
||
def _on_stop(self, active): | ||
""" Trait handler that stops event loop. """ | ||
if not active: | ||
self.gui.stop_event_loop() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd really like to include some advice in the docstring that if you find yourself needing to use a
repeat
of greater than1
, you should consider whether you can useevent_loop_until_condition
instead.