test-kivy-app/kivy_venv/lib/python3.11/site-packages/kivy/tests/async_common.py

569 lines
19 KiB
Python
Raw Normal View History

2024-09-15 12:12:16 +00:00
"""
.. warning::
The classes in this file are internal and may well be removed to an
external kivy-pytest package or similar in the future. Use at your own
risk.
"""
import random
import time
import math
import os
from collections import deque
from kivy.tests import UnitTestTouch
__all__ = ('UnitKivyApp', )
class AsyncUnitTestTouch(UnitTestTouch):
def __init__(self, *largs, **kwargs):
self.grab_exclusive_class = None
self.is_touch = True
super(AsyncUnitTestTouch, self).__init__(*largs, **kwargs)
def touch_down(self, *args):
self.eventloop._dispatch_input("begin", self)
def touch_move(self, x, y):
win = self.eventloop.window
self.move({
"x": x / (win.width - 1.0),
"y": y / (win.height - 1.0)
})
self.eventloop._dispatch_input("update", self)
def touch_up(self, *args):
self.eventloop._dispatch_input("end", self)
_unique_value = object
class WidgetResolver(object):
"""It assumes that the widget tree strictly forms a DAG.
"""
base_widget = None
matched_widget = None
_kwargs_filter = {}
_funcs_filter = []
def __init__(self, base_widget, **kwargs):
self.base_widget = base_widget
self._kwargs_filter = {}
self._funcs_filter = []
super(WidgetResolver, self).__init__(**kwargs)
def __call__(self):
if self.matched_widget is not None:
return self.matched_widget
if not self._kwargs_filter and not self._funcs_filter:
return self.base_widget
return None
def match(self, **kwargs_filter):
self._kwargs_filter.update(kwargs_filter)
def match_funcs(self, funcs_filter=()):
self._funcs_filter.extend(funcs_filter)
def check_widget(self, widget):
if not all(func(widget) for func in self._funcs_filter):
return False
for attr, val in self._kwargs_filter.items():
if getattr(widget, attr, _unique_value) != val:
return False
return True
def not_found(self, op):
raise ValueError(
'Cannot find widget matching <{}, {}> starting from base '
'widget "{}" doing "{}" traversal'.format(
self._kwargs_filter, self._funcs_filter, self.base_widget, op))
def down(self, **kwargs_filter):
self.match(**kwargs_filter)
check = self.check_widget
fifo = deque([self.base_widget])
while fifo:
widget = fifo.popleft()
if check(widget):
return WidgetResolver(base_widget=widget)
fifo.extend(widget.children)
self.not_found('down')
def up(self, **kwargs_filter):
self.match(**kwargs_filter)
check = self.check_widget
parent = self.base_widget
while parent is not None:
if check(parent):
return WidgetResolver(base_widget=parent)
new_parent = parent.parent
# Window is its own parent oO
if new_parent is parent:
break
parent = new_parent
self.not_found('up')
def family_up(self, **kwargs_filter):
self.match(**kwargs_filter)
check = self.check_widget
base_widget = self.base_widget
already_checked_base = None
while base_widget is not None:
fifo = deque([base_widget])
while fifo:
widget = fifo.popleft()
# don't check the child we checked before moving up
if widget is already_checked_base:
continue
if check(widget):
return WidgetResolver(base_widget=widget)
fifo.extend(widget.children)
already_checked_base = base_widget
new_base_widget = base_widget.parent
# Window is its own parent oO
if new_base_widget is base_widget:
break
base_widget = new_base_widget
self.not_found('family_up')
class UnitKivyApp(object):
"""Base class to use with async test apps.
.. warning::
The classes in this file are internal and may well be removed to an
external kivy-pytest package or similar in the future. Use at your own
risk.
"""
app_has_started = False
app_has_stopped = False
async_sleep = None
def __init__(self, **kwargs):
super().__init__(**kwargs)
def started_app(*largs):
self.app_has_started = True
self.fbind('on_start', started_app)
def stopped_app(*largs):
self.app_has_stopped = True
self.fbind('on_stop', stopped_app)
def set_async_lib(self, async_lib):
from kivy.clock import Clock
if async_lib is not None:
Clock.init_async_lib(async_lib)
self.async_sleep = Clock._async_lib.sleep
async def async_run(self, async_lib=None):
self.set_async_lib(async_lib)
return await super(UnitKivyApp, self).async_run(async_lib=async_lib)
def resolve_widget(self, base_widget=None):
if base_widget is None:
from kivy.core.window import Window
base_widget = Window
return WidgetResolver(base_widget=base_widget)
async def wait_clock_frames(self, n, sleep_time=1 / 60.):
from kivy.clock import Clock
frames_start = Clock.frames
while Clock.frames < frames_start + n:
await self.async_sleep(sleep_time)
def get_widget_pos_pixel(self, widget, positions):
from kivy.graphics import Fbo, ClearColor, ClearBuffers
canvas_parent_index = -2
if widget.parent is not None:
canvas_parent_index = widget.parent.canvas.indexof(widget.canvas)
if canvas_parent_index > -1:
widget.parent.canvas.remove(widget.canvas)
w, h = int(widget.width), int(widget.height)
fbo = Fbo(size=(w, h), with_stencilbuffer=True)
with fbo:
ClearColor(0, 0, 0, 0)
ClearBuffers()
fbo.add(widget.canvas)
fbo.draw()
pixels = fbo.pixels
fbo.remove(widget.canvas)
if widget.parent is not None and canvas_parent_index > -1:
widget.parent.canvas.insert(canvas_parent_index, widget.canvas)
values = []
for x, y in positions:
x = int(x)
y = int(y)
i = y * w * 4 + x * 4
values.append(tuple(pixels[i:i + 4]))
return values
async def do_touch_down_up(
self, pos=None, widget=None, duration=.2, pos_jitter=None,
widget_jitter=False, jitter_dt=1 / 15., end_on_pos=False):
if widget is None:
x, y = pos
else:
if pos is None:
x, y = widget.to_window(*widget.center)
else:
x, y = widget.to_window(*pos, initial=False)
touch = AsyncUnitTestTouch(x, y)
ts = time.perf_counter()
touch.touch_down()
await self.wait_clock_frames(1)
yield 'down', touch.pos
if not pos_jitter and not widget_jitter:
await self.async_sleep(duration)
touch.touch_up()
await self.wait_clock_frames(1)
yield 'up', touch.pos
return
moved = False
if pos_jitter:
dx, dy = pos_jitter
else:
dx = widget.width / 2.
dy = widget.height / 2.
while time.perf_counter() - ts < duration:
moved = True
await self.async_sleep(jitter_dt)
touch.touch_move(
x + (random.random() * 2 - 1) * dx,
y + (random.random() * 2 - 1) * dy
)
await self.wait_clock_frames(1)
yield 'move', touch.pos
if end_on_pos and moved:
touch.touch_move(x, y)
await self.wait_clock_frames(1)
yield 'move', touch.pos
touch.touch_up()
await self.wait_clock_frames(1)
yield 'up', touch.pos
async def do_touch_drag(
self, pos=None, widget=None,
widget_loc=('center_x', 'center_y'), dx=0, dy=0,
target_pos=None, target_widget=None, target_widget_offset=(0, 0),
target_widget_loc=('center_x', 'center_y'), long_press=0,
duration=.2, drag_n=5):
"""Initiates a touch down, followed by some dragging to a target
location, ending with a touch up.
`origin`: These parameters specify where the drag starts.
- If ``widget`` is None, it starts at ``pos`` (in window coordinates).
If ``dx``/``dy`` is used, it is in the window coordinate system also.
- If ``pos`` is None, it starts on the ``widget`` as specified by
``widget_loc``. If ``dx``/``dy`` is used, it is in the ``widget``
coordinate system.
- If neither is None, it starts at ``pos``, but in the ``widget``'s
coordinate system (:meth:`~kivy.uix.widget.Widget.to_window` is used
on it). If ``dx``/``dy`` is used, it is in the ``widget``
coordinate system.
`target`: These parameters specify where the drag ends.
- If ``target_pos`` and ``target_widget`` is None, then ``dx`` and
``dy`` is used relative to the position where the drag started.
- If ``target_widget`` is None, it ends at ``target_pos``
(in window coordinates).
- If ``target_pos`` is None, it ends on the ``target_widget`` as
specified by ``target_widget_loc``. ``target_widget_offset``, is an
additional ``(x, y)`` offset relative to ``target_widget_loc``.
- If neither is None, it starts at ``target_pos``, but in the
``target_widget``'s coordinate system
(:meth:`~kivy.uix.widget.Widget.to_window` is used on it).
When ``widget`` and/or ``target_widget`` are specified, ``widget_loc``
and ``target_widget_loc``, respectively, indicate where on the widget
the drag starts/ends. It is a a tuple with property names of the widget
to loop up to get the value. The default is
``('center_x', 'center_y')`` so the drag would start/end in the
widget's center.
"""
if widget is None:
x, y = pos
tx, ty = x + dx, y + dy
else:
if pos is None:
w_x = getattr(widget, widget_loc[0])
w_y = getattr(widget, widget_loc[1])
x, y = widget.to_window(w_x, w_y)
tx, ty = widget.to_window(w_x + dx, w_y + dy)
else:
x, y = widget.to_window(*pos, initial=False)
tx, ty = widget.to_window(
pos[0] + dx, pos[1] + dy, initial=False)
if target_pos is not None:
if target_widget is None:
tx, ty = target_pos
else:
tx, ty = target_pos = target_widget.to_window(
*target_pos, initial=False)
elif target_widget is not None:
x_off, y_off = target_widget_offset
w_x = getattr(target_widget, target_widget_loc[0]) + x_off
w_y = getattr(target_widget, target_widget_loc[1]) + y_off
tx, ty = target_pos = target_widget.to_window(w_x, w_y)
else:
target_pos = tx, ty
touch = AsyncUnitTestTouch(x, y)
touch.touch_down()
await self.wait_clock_frames(1)
if long_press:
await self.async_sleep(long_press)
yield 'down', touch.pos
dx = (tx - x) / drag_n
dy = (ty - y) / drag_n
ts0 = time.perf_counter()
for i in range(drag_n):
await self.async_sleep(
max(0., duration - (time.perf_counter() - ts0)) / (drag_n - i))
touch.touch_move(x + (i + 1) * dx, y + (i + 1) * dy)
await self.wait_clock_frames(1)
yield 'move', touch.pos
if touch.pos != target_pos:
touch.touch_move(*target_pos)
await self.wait_clock_frames(1)
yield 'move', touch.pos
touch.touch_up()
await self.wait_clock_frames(1)
yield 'up', touch.pos
async def do_touch_drag_follow(
self, pos=None, widget=None,
widget_loc=('center_x', 'center_y'),
target_pos=None, target_widget=None, target_widget_offset=(0, 0),
target_widget_loc=('center_x', 'center_y'), long_press=0,
duration=.2, drag_n=5, max_n=25):
"""Very similar to :meth:`do_touch_drag`, except it follows the target
widget, even if the target widget moves as a result of the drag, the
drag will follow it until it's on the target widget.
`origin`: These parameters specify where the drag starts.
- If ``widget`` is None, it starts at ``pos`` (in window coordinates).
- If ``pos`` is None, it starts on the ``widget`` as specified by
``widget_loc``.
- If neither is None, it starts at ``pos``, but in the ``widget``'s
coordinate system (:meth:`~kivy.uix.widget.Widget.to_window` is used
on it).
`target`: These parameters specify where the drag ends.
- If ``target_pos`` is None, it ends on the ``target_widget`` as
specified by ``target_widget_loc``. ``target_widget_offset``, is an
additional ``(x, y)`` offset relative to ``target_widget_loc``.
- If ``target_pos`` is not None, it starts at ``target_pos``, but in
the ``target_widget``'s coordinate system
(:meth:`~kivy.uix.widget.Widget.to_window` is used on it).
When ``widget`` and/or ``target_widget`` are specified, ``widget_loc``
and ``target_widget_loc``, respectively, indicate where on the widget
the drag starts/ends. It is a a tuple with property names of the widget
to loop up to get the value. The default is
``('center_x', 'center_y')`` so the drag would start/end in the
widget's center.
"""
if widget is None:
x, y = pos
else:
if pos is None:
w_x = getattr(widget, widget_loc[0])
w_y = getattr(widget, widget_loc[1])
x, y = widget.to_window(w_x, w_y)
else:
x, y = widget.to_window(*pos, initial=False)
if target_widget is None:
raise ValueError('target_widget must be specified')
def get_target():
if target_pos is not None:
return target_widget.to_window(*target_pos, initial=False)
else:
x_off, y_off = target_widget_offset
wt_x = getattr(target_widget, target_widget_loc[0]) + x_off
wt_y = getattr(target_widget, target_widget_loc[1]) + y_off
return target_widget.to_window(wt_x, wt_y)
touch = AsyncUnitTestTouch(x, y)
touch.touch_down()
await self.wait_clock_frames(1)
if long_press:
await self.async_sleep(long_press)
yield 'down', touch.pos
ts0 = time.perf_counter()
tx, ty = get_target()
i = 0
while not (math.isclose(touch.x, tx) and math.isclose(touch.y, ty)):
if i >= max_n:
raise Exception(
'Exceeded the maximum number of iterations, '
'but {} != {}'.format(touch.pos, (tx, ty)))
rem_i = max(1, drag_n - i)
rem_t = max(0., duration - (time.perf_counter() - ts0)) / rem_i
i += 1
await self.async_sleep(rem_t)
x, y = touch.pos
touch.touch_move(x + (tx - x) / rem_i, y + (ty - y) / rem_i)
await self.wait_clock_frames(1)
yield 'move', touch.pos
tx, ty = get_target()
touch.touch_up()
await self.wait_clock_frames(1)
yield 'up', touch.pos
async def do_touch_drag_path(
self, path, axis_widget=None, long_press=0, duration=.2):
"""Drags the touch along the specified path.
:parameters:
`path`: list
A list of position tuples the touch will follow. The first
item is used for the touch down and the rest for the move.
`axis_widget`: a Widget
If None, the path coordinates is in window coordinates,
otherwise, we will first transform the path coordinates
to window coordinates using
:meth:`~kivy.uix.widget.Widget.to_window` of the specified
widget.
"""
if axis_widget is not None:
path = [axis_widget.to_window(*p, initial=False) for p in path]
x, y = path[0]
path = path[1:]
touch = AsyncUnitTestTouch(x, y)
touch.touch_down()
await self.wait_clock_frames(1)
if long_press:
await self.async_sleep(long_press)
yield 'down', touch.pos
ts0 = time.perf_counter()
n = len(path)
for i, (x2, y2) in enumerate(path):
await self.async_sleep(
max(0., duration - (time.perf_counter() - ts0)) / (n - i))
touch.touch_move(x2, y2)
await self.wait_clock_frames(1)
yield 'move', touch.pos
touch.touch_up()
await self.wait_clock_frames(1)
yield 'up', touch.pos
async def do_keyboard_key(
self, key, modifiers=(), duration=.05, num_press=1):
from kivy.core.window import Window
if key == ' ':
key = 'spacebar'
key_lower = key.lower()
key_code = Window._system_keyboard.string_to_keycode(key_lower)
known_modifiers = {'shift', 'alt', 'ctrl', 'meta'}
if set(modifiers) - known_modifiers:
raise ValueError('Unknown modifiers "{}"'.
format(set(modifiers) - known_modifiers))
special_keys = {
27: 'escape',
9: 'tab',
8: 'backspace',
13: 'enter',
127: 'del',
271: 'enter',
273: 'up',
274: 'down',
275: 'right',
276: 'left',
278: 'home',
279: 'end',
280: 'pgup',
281: 'pgdown',
300: 'numlock',
301: 'capslock',
145: 'screenlock',
}
text = None
try:
text = chr(key_code)
if key_lower != key:
text = key
except ValueError:
pass
dt = duration / num_press
for i in range(num_press):
await self.async_sleep(dt)
Window.dispatch('on_key_down', key_code, 0, text, modifiers)
if (key not in known_modifiers and
key_code not in special_keys and
not (known_modifiers & set(modifiers))):
Window.dispatch('on_textinput', text)
await self.wait_clock_frames(1)
yield 'down', (key, key_code, 0, text, modifiers)
Window.dispatch('on_key_up', key_code, 0)
await self.wait_clock_frames(1)
yield 'up', (key, key_code, 0, text, modifiers)