first commit
This commit is contained in:
commit
417e54da96
5696 changed files with 900003 additions and 0 deletions
|
@ -0,0 +1,11 @@
|
|||
from kivy.tests.common import GraphicUnitTest, UnitTestTouch, UTMotionEvent, \
|
||||
async_run
|
||||
try:
|
||||
from kivy.tests.async_common import UnitKivyApp
|
||||
except SyntaxError:
|
||||
# async app tests would be skipped due to async_run forcing it to skip so
|
||||
# it's ok to be None as it won't be used anyway
|
||||
UnitKivyApp = None
|
||||
|
||||
__all__ = ('GraphicUnitTest', 'UnitTestTouch', 'UTMotionEvent', 'async_run',
|
||||
'UnitKivyApp')
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,568 @@
|
|||
"""
|
||||
.. 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)
|
524
kivy_venv/lib/python3.11/site-packages/kivy/tests/common.py
Normal file
524
kivy_venv/lib/python3.11/site-packages/kivy/tests/common.py
Normal file
|
@ -0,0 +1,524 @@
|
|||
'''
|
||||
This is a extended unittest module for Kivy, to make unittests based on
|
||||
graphics with an OpenGL context.
|
||||
|
||||
The idea is to render a Widget tree, and after 1, 2 or more frames, a
|
||||
screenshot will be made and be compared to the original one.
|
||||
If no screenshot exists for the current test, the very first one will be used.
|
||||
|
||||
The screenshots live in the 'kivy/tests/results' folder and are in PNG format,
|
||||
320x240 pixels.
|
||||
'''
|
||||
|
||||
__all__ = (
|
||||
'GraphicUnitTest', 'UnitTestTouch', 'UTMotionEvent', 'async_run',
|
||||
'requires_graphics', 'ensure_web_server')
|
||||
|
||||
import unittest
|
||||
import logging
|
||||
import pytest
|
||||
import sys
|
||||
from functools import partial
|
||||
import os
|
||||
import threading
|
||||
from kivy.graphics.cgl import cgl_get_backend_name
|
||||
from kivy.input.motionevent import MotionEvent
|
||||
log = logging.getLogger('unittest')
|
||||
|
||||
|
||||
_base = object
|
||||
if 'mock' != cgl_get_backend_name():
|
||||
# check what the gl backend might be, we can't know for sure
|
||||
# what it'll be until actually initialized by the window.
|
||||
_base = unittest.TestCase
|
||||
|
||||
make_screenshots = os.environ.get('KIVY_UNITTEST_SCREENSHOTS')
|
||||
http_server = None
|
||||
http_server_ready = threading.Event()
|
||||
kivy_eventloop = os.environ.get('KIVY_EVENTLOOP', 'asyncio')
|
||||
|
||||
|
||||
def requires_graphics(func):
|
||||
if 'mock' == cgl_get_backend_name():
|
||||
return pytest.mark.skip(
|
||||
reason='Skipping because gl backend is set to mock')(func)
|
||||
return func
|
||||
|
||||
|
||||
def ensure_web_server(root=None):
|
||||
if http_server is not None:
|
||||
return True
|
||||
|
||||
if not root:
|
||||
root = os.path.join(os.path.dirname(__file__), "..", "..")
|
||||
need_chdir = sys.version_info.major == 3 and sys.version_info.minor <= 6
|
||||
curr_dir = os.getcwd()
|
||||
|
||||
def _start_web_server():
|
||||
global http_server
|
||||
from http.server import SimpleHTTPRequestHandler
|
||||
from socketserver import TCPServer
|
||||
|
||||
try:
|
||||
if need_chdir:
|
||||
os.chdir(root)
|
||||
handler = SimpleHTTPRequestHandler
|
||||
else:
|
||||
handler = partial(SimpleHTTPRequestHandler, directory=root)
|
||||
|
||||
http_server = TCPServer(
|
||||
("", 8000), handler, bind_and_activate=False)
|
||||
http_server.daemon_threads = True
|
||||
http_server.allow_reuse_address = True
|
||||
http_server.server_bind()
|
||||
http_server.server_activate()
|
||||
http_server_ready.set()
|
||||
http_server.serve_forever()
|
||||
except:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
finally:
|
||||
http_server = None
|
||||
http_server_ready.set()
|
||||
|
||||
if need_chdir:
|
||||
os.chdir(curr_dir)
|
||||
|
||||
th = threading.Thread(target=_start_web_server)
|
||||
th.daemon = True
|
||||
th.start()
|
||||
http_server_ready.wait()
|
||||
if http_server is None:
|
||||
raise Exception("Unable to start webserver")
|
||||
|
||||
|
||||
class GraphicUnitTest(_base):
|
||||
framecount = 0
|
||||
|
||||
def _force_refresh(self, *largs):
|
||||
# this prevent in some case to be stuck if the screen doesn't refresh
|
||||
# and we wait for a number of self.framecount that never goes down
|
||||
from kivy.base import EventLoop
|
||||
win = EventLoop.window
|
||||
if win and win.canvas:
|
||||
win.canvas.ask_update()
|
||||
|
||||
def render(self, root, framecount=1):
|
||||
'''Call rendering process using the `root` widget.
|
||||
The screenshot will be done in `framecount` frames.
|
||||
'''
|
||||
from kivy.base import runTouchApp
|
||||
from kivy.clock import Clock
|
||||
self.framecount = framecount
|
||||
try:
|
||||
Clock.schedule_interval(self._force_refresh, 1)
|
||||
runTouchApp(root)
|
||||
finally:
|
||||
Clock.unschedule(self._force_refresh)
|
||||
|
||||
# reset for the next test, but nobody will know if it will be used :/
|
||||
if self.test_counter != 0:
|
||||
self.tearDown(fake=True)
|
||||
self.setUp()
|
||||
|
||||
def run(self, *args, **kwargs):
|
||||
'''Extend the run of unittest, to check if results directory have been
|
||||
found. If no results directory exists, the test will be ignored.
|
||||
'''
|
||||
from os.path import join, dirname, exists
|
||||
results_dir = join(dirname(__file__), 'results')
|
||||
if make_screenshots and not exists(results_dir):
|
||||
log.warning('No result directory found, cancel test.')
|
||||
os.mkdir(results_dir)
|
||||
self.test_counter = 0
|
||||
self.results_dir = results_dir
|
||||
self.test_failed = False
|
||||
return super(GraphicUnitTest, self).run(*args, **kwargs)
|
||||
|
||||
def setUp(self):
|
||||
'''Prepare the graphic test, with:
|
||||
- Window size fixed to 320x240
|
||||
- Default kivy configuration
|
||||
- Without any kivy input
|
||||
'''
|
||||
|
||||
# use default kivy configuration (don't load user file.)
|
||||
from os import environ
|
||||
environ['KIVY_USE_DEFAULTCONFIG'] = '1'
|
||||
|
||||
# force window size + remove all inputs
|
||||
from kivy.config import Config
|
||||
Config.set('graphics', 'width', '320')
|
||||
Config.set('graphics', 'height', '240')
|
||||
for items in Config.items('input'):
|
||||
Config.remove_option('input', items[0])
|
||||
|
||||
# bind ourself for the later screenshot
|
||||
from kivy.core.window import Window
|
||||
self.Window = Window
|
||||
Window.bind(on_flip=self.on_window_flip)
|
||||
|
||||
# ensure our window is correctly created
|
||||
Window.create_window()
|
||||
Window.register()
|
||||
Window.initialized = True
|
||||
Window.close = lambda *s: None
|
||||
self.clear_window_and_event_loop()
|
||||
|
||||
def clear_window_and_event_loop(self):
|
||||
from kivy.base import EventLoop
|
||||
window = self.Window
|
||||
for child in window.children[:]:
|
||||
window.remove_widget(child)
|
||||
window.canvas.before.clear()
|
||||
window.canvas.clear()
|
||||
window.canvas.after.clear()
|
||||
EventLoop.touches.clear()
|
||||
for post_proc in EventLoop.postproc_modules:
|
||||
if hasattr(post_proc, 'touches'):
|
||||
post_proc.touches.clear()
|
||||
elif hasattr(post_proc, 'last_touches'):
|
||||
post_proc.last_touches.clear()
|
||||
|
||||
def on_window_flip(self, window):
|
||||
'''Internal method to be called when the window have just displayed an
|
||||
image.
|
||||
When an image is showed, we decrement our framecount. If framecount is
|
||||
come to 0, we are taking the screenshot.
|
||||
|
||||
The screenshot is done in a temporary place, and is compared to the
|
||||
original one -> test ok/ko.
|
||||
If no screenshot is available in the results directory, a new one will
|
||||
be created.
|
||||
'''
|
||||
from kivy.base import EventLoop
|
||||
from tempfile import mkstemp
|
||||
from os.path import join, exists
|
||||
from os import unlink, close
|
||||
from shutil import move, copy
|
||||
|
||||
# don't save screenshot until we have enough frames.
|
||||
# log.debug('framecount %d' % self.framecount)
|
||||
# ! check if there is 'framecount', otherwise just
|
||||
# ! assume zero e.g. if handling runTouchApp manually
|
||||
self.framecount = getattr(self, 'framecount', 0) - 1
|
||||
if self.framecount > 0:
|
||||
return
|
||||
|
||||
# don't create screenshots if not requested manually
|
||||
if not make_screenshots:
|
||||
EventLoop.stop()
|
||||
return
|
||||
|
||||
reffn = None
|
||||
match = False
|
||||
try:
|
||||
# just get a temporary name
|
||||
fd, tmpfn = mkstemp(suffix='.png', prefix='kivyunit-')
|
||||
close(fd)
|
||||
unlink(tmpfn)
|
||||
|
||||
# get a filename for the current unit test
|
||||
self.test_counter += 1
|
||||
test_uid = '%s-%d.png' % (
|
||||
'_'.join(self.id().split('.')[-2:]),
|
||||
self.test_counter)
|
||||
|
||||
# capture the screen
|
||||
log.info('Capturing screenshot for %s' % test_uid)
|
||||
tmpfn = window.screenshot(tmpfn)
|
||||
log.info('Capture saved at %s' % tmpfn)
|
||||
|
||||
# search the file to compare to
|
||||
reffn = join(self.results_dir, test_uid)
|
||||
log.info('Compare with %s' % reffn)
|
||||
|
||||
# get sourcecode
|
||||
import inspect
|
||||
frame = inspect.getouterframes(inspect.currentframe())[6]
|
||||
sourcecodetab, line = inspect.getsourcelines(frame[0])
|
||||
line = frame[2] - line
|
||||
currentline = sourcecodetab[line]
|
||||
sourcecodetab[line] = '<span style="color: red;">%s</span>' % (
|
||||
currentline)
|
||||
sourcecode = ''.join(sourcecodetab)
|
||||
sourcecodetab[line] = '>>>>>>>>\n%s<<<<<<<<\n' % currentline
|
||||
sourcecodeask = ''.join(sourcecodetab)
|
||||
|
||||
if not exists(reffn):
|
||||
log.info('No image reference, move %s as ref ?' % test_uid)
|
||||
if self.interactive_ask_ref(sourcecodeask, tmpfn, self.id()):
|
||||
move(tmpfn, reffn)
|
||||
tmpfn = reffn
|
||||
log.info('Image used as reference')
|
||||
match = True
|
||||
else:
|
||||
log.info('Image discarded')
|
||||
else:
|
||||
from kivy.core.image import Image as CoreImage
|
||||
s1 = CoreImage(tmpfn, keep_data=True)
|
||||
sd1 = s1.image._data[0].data
|
||||
s2 = CoreImage(reffn, keep_data=True)
|
||||
sd2 = s2.image._data[0].data
|
||||
if sd1 != sd2:
|
||||
log.critical(
|
||||
'%s at render() #%d, images are different.' % (
|
||||
self.id(), self.test_counter))
|
||||
if self.interactive_ask_diff(sourcecodeask,
|
||||
tmpfn, reffn, self.id()):
|
||||
log.critical('user ask to use it as ref.')
|
||||
move(tmpfn, reffn)
|
||||
tmpfn = reffn
|
||||
match = True
|
||||
else:
|
||||
self.test_failed = True
|
||||
else:
|
||||
match = True
|
||||
|
||||
# generate html
|
||||
from os.path import join, dirname, exists, basename
|
||||
from os import mkdir
|
||||
build_dir = join(dirname(__file__), 'build')
|
||||
if not exists(build_dir):
|
||||
mkdir(build_dir)
|
||||
copy(reffn, join(build_dir, 'ref_%s' % basename(reffn)))
|
||||
if tmpfn != reffn:
|
||||
copy(tmpfn, join(build_dir, 'test_%s' % basename(reffn)))
|
||||
with open(join(build_dir, 'index.html'), 'at') as fd:
|
||||
color = '#ffdddd' if not match else '#ffffff'
|
||||
fd.write('<div style="background-color: %s">' % color)
|
||||
fd.write('<h2>%s #%d</h2>' % (self.id(), self.test_counter))
|
||||
fd.write('<table><tr><th>Reference</th>'
|
||||
'<th>Test</th>'
|
||||
'<th>Comment</th>')
|
||||
fd.write('<tr><td><img src="ref_%s"/></td>' %
|
||||
basename(reffn))
|
||||
if tmpfn != reffn:
|
||||
fd.write('<td><img src="test_%s"/></td>' %
|
||||
basename(reffn))
|
||||
else:
|
||||
fd.write('<td>First time, no comparison.</td>')
|
||||
fd.write('<td><pre>%s</pre></td>' % sourcecode)
|
||||
fd.write('</table></div>')
|
||||
finally:
|
||||
try:
|
||||
if reffn != tmpfn:
|
||||
unlink(tmpfn)
|
||||
except:
|
||||
pass
|
||||
EventLoop.stop()
|
||||
|
||||
def tearDown(self, fake=False):
|
||||
'''When the test is finished, stop the application, and unbind our
|
||||
current flip callback.
|
||||
'''
|
||||
from kivy.base import stopTouchApp
|
||||
from kivy.core.window import Window
|
||||
Window.unbind(on_flip=self.on_window_flip)
|
||||
self.clear_window_and_event_loop()
|
||||
self.Window = None
|
||||
stopTouchApp()
|
||||
if not fake and self.test_failed:
|
||||
self.assertTrue(False)
|
||||
super(GraphicUnitTest, self).tearDown()
|
||||
|
||||
def interactive_ask_ref(self, code, imagefn, testid):
|
||||
from os import environ
|
||||
if 'UNITTEST_INTERACTIVE' not in environ:
|
||||
return True
|
||||
|
||||
from tkinter import Tk, Label, LEFT, RIGHT, BOTTOM, Button
|
||||
from PIL import Image, ImageTk
|
||||
|
||||
self.retval = False
|
||||
|
||||
root = Tk()
|
||||
|
||||
def do_close():
|
||||
root.destroy()
|
||||
|
||||
def do_yes():
|
||||
self.retval = True
|
||||
do_close()
|
||||
|
||||
image = Image.open(imagefn)
|
||||
photo = ImageTk.PhotoImage(image)
|
||||
Label(root, text='The test %s\nhave no reference.' % testid).pack()
|
||||
Label(root, text='Use this image as a reference ?').pack()
|
||||
Label(root, text=code, justify=LEFT).pack(side=RIGHT)
|
||||
Label(root, image=photo).pack(side=LEFT)
|
||||
Button(root, text='Use as reference', command=do_yes).pack(side=BOTTOM)
|
||||
Button(root, text='Discard', command=do_close).pack(side=BOTTOM)
|
||||
root.mainloop()
|
||||
|
||||
return self.retval
|
||||
|
||||
def interactive_ask_diff(self, code, tmpfn, reffn, testid):
|
||||
from os import environ
|
||||
if 'UNITTEST_INTERACTIVE' not in environ:
|
||||
return False
|
||||
|
||||
from tkinter import Tk, Label, LEFT, RIGHT, BOTTOM, Button
|
||||
from PIL import Image, ImageTk
|
||||
|
||||
self.retval = False
|
||||
|
||||
root = Tk()
|
||||
|
||||
def do_close():
|
||||
root.destroy()
|
||||
|
||||
def do_yes():
|
||||
self.retval = True
|
||||
do_close()
|
||||
|
||||
phototmp = ImageTk.PhotoImage(Image.open(tmpfn))
|
||||
photoref = ImageTk.PhotoImage(Image.open(reffn))
|
||||
Label(root, text='The test %s\nhave generated an different'
|
||||
'image as the reference one..' % testid).pack()
|
||||
Label(root, text='Which one is good ?').pack()
|
||||
Label(root, text=code, justify=LEFT).pack(side=RIGHT)
|
||||
Label(root, image=phototmp).pack(side=RIGHT)
|
||||
Label(root, image=photoref).pack(side=LEFT)
|
||||
Button(root, text='Use the new image -->',
|
||||
command=do_yes).pack(side=BOTTOM)
|
||||
Button(root, text='<-- Use the reference',
|
||||
command=do_close).pack(side=BOTTOM)
|
||||
root.mainloop()
|
||||
|
||||
return self.retval
|
||||
|
||||
def advance_frames(self, count):
|
||||
'''Render the new frames and:
|
||||
|
||||
* tick the Clock
|
||||
* dispatch input from all registered providers
|
||||
* flush all the canvas operations
|
||||
* redraw Window canvas if necessary
|
||||
'''
|
||||
from kivy.base import EventLoop
|
||||
for i in range(count):
|
||||
EventLoop.idle()
|
||||
|
||||
|
||||
class UnitTestTouch(MotionEvent):
|
||||
'''Custom MotionEvent representing a single touch. Similar to `on_touch_*`
|
||||
methods from the Widget class, this one introduces:
|
||||
|
||||
* touch_down
|
||||
* touch_move
|
||||
* touch_up
|
||||
|
||||
Create a new touch with::
|
||||
|
||||
touch = UnitTestTouch(x, y)
|
||||
|
||||
then you press it on the default position with::
|
||||
|
||||
touch.touch_down()
|
||||
|
||||
or move it or even release with these simple calls::
|
||||
|
||||
touch.touch_move(new_x, new_y)
|
||||
touch.touch_up()
|
||||
'''
|
||||
|
||||
def __init__(self, x, y):
|
||||
'''Create a MotionEvent instance with X and Y of the first
|
||||
position a touch is at.
|
||||
'''
|
||||
from kivy.base import EventLoop
|
||||
self.eventloop = EventLoop
|
||||
win = EventLoop.window
|
||||
super(UnitTestTouch, self).__init__(
|
||||
# device, (tuio) id, args
|
||||
self.__class__.__name__, 99, {
|
||||
"x": x / (win.width - 1.0),
|
||||
"y": y / (win.height - 1.0),
|
||||
},
|
||||
is_touch=True,
|
||||
type_id='touch'
|
||||
)
|
||||
# set profile to accept x, y and pos properties
|
||||
self.profile = ['pos']
|
||||
|
||||
def touch_down(self, *args):
|
||||
self.eventloop.post_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.post_dispatch_input("update", self)
|
||||
|
||||
def touch_up(self, *args):
|
||||
self.eventloop.post_dispatch_input("end", self)
|
||||
|
||||
def depack(self, args):
|
||||
# set sx/sy properties to ratio (e.g. X / win.width)
|
||||
self.sx = args['x']
|
||||
self.sy = args['y']
|
||||
# run depack after we set the values
|
||||
super().depack(args)
|
||||
|
||||
|
||||
# https://gist.github.com/tito/f111b6916aa6a4ed0851
|
||||
# subclass for touch event in unit test
|
||||
class UTMotionEvent(MotionEvent):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs.setdefault('is_touch', True)
|
||||
kwargs.setdefault('type_id', 'touch')
|
||||
super().__init__(*args, **kwargs)
|
||||
self.profile = ['pos']
|
||||
|
||||
def depack(self, args):
|
||||
self.sx = args['x']
|
||||
self.sy = args['y']
|
||||
super().depack(args)
|
||||
|
||||
|
||||
def async_run(func=None, app_cls_func=None):
|
||||
def inner_func(func):
|
||||
if 'mock' == cgl_get_backend_name():
|
||||
return pytest.mark.skip(
|
||||
reason='Skipping because gl backend is set to mock')(func)
|
||||
|
||||
if sys.version_info[0] < 3 or sys.version_info[1] <= 5:
|
||||
return pytest.mark.skip(
|
||||
reason='Skipping because graphics tests are not supported on '
|
||||
'py3.5, only on py3.6+')(func)
|
||||
|
||||
if app_cls_func is not None:
|
||||
func = pytest.mark.parametrize(
|
||||
"kivy_app", [[app_cls_func], ], indirect=True)(func)
|
||||
|
||||
if kivy_eventloop == 'asyncio':
|
||||
try:
|
||||
import pytest_asyncio
|
||||
return pytest.mark.asyncio(pytest_asyncio.fixture(func))
|
||||
except ImportError:
|
||||
return pytest.mark.skip(
|
||||
reason='KIVY_EVENTLOOP == "asyncio" but '
|
||||
'"pytest-asyncio" is not installed')(func)
|
||||
elif kivy_eventloop == 'trio':
|
||||
try:
|
||||
import trio
|
||||
from pytest_trio import trio_fixture
|
||||
func._force_trio_fixture = True
|
||||
return func
|
||||
except ImportError:
|
||||
return pytest.mark.skip(
|
||||
reason='KIVY_EVENTLOOP == "trio" but '
|
||||
'"pytest-trio" is not installed')(func)
|
||||
else:
|
||||
return pytest.mark.skip(
|
||||
reason='KIVY_EVENTLOOP must be set to either of "asyncio" or '
|
||||
'"trio" to run async tests')(func)
|
||||
|
||||
if func is None:
|
||||
return inner_func
|
||||
|
||||
return inner_func(func)
|
|
@ -0,0 +1,33 @@
|
|||
import pytest
|
||||
import os
|
||||
|
||||
kivy_eventloop = os.environ.get('KIVY_EVENTLOOP', 'asyncio')
|
||||
|
||||
try:
|
||||
from .fixtures import kivy_app, kivy_clock, kivy_metrics, \
|
||||
kivy_exception_manager
|
||||
except SyntaxError:
|
||||
# async app tests would be skipped due to async_run forcing it to skip so
|
||||
# it's ok to fail here as it won't be used anyway
|
||||
pass
|
||||
|
||||
if kivy_eventloop != 'trio':
|
||||
@pytest.fixture()
|
||||
def nursery():
|
||||
pass
|
||||
|
||||
|
||||
def pytest_runtest_makereport(item, call):
|
||||
# from https://docs.pytest.org/en/latest/example/simple.html
|
||||
if "incremental" in item.keywords:
|
||||
if call.excinfo is not None:
|
||||
parent = item.parent
|
||||
parent._previousfailed = item
|
||||
|
||||
|
||||
def pytest_runtest_setup(item):
|
||||
# from https://docs.pytest.org/en/latest/example/simple.html
|
||||
if "incremental" in item.keywords:
|
||||
previousfailed = getattr(item.parent, "_previousfailed", None)
|
||||
if previousfailed is not None:
|
||||
pytest.xfail("previous test failed (%s)" % previousfailed.name)
|
|
@ -0,0 +1,17 @@
|
|||
#:import kivy kivy
|
||||
|
||||
<SomeWidget@Widget>:
|
||||
on_x: self.something = 42
|
||||
height: self.width
|
||||
width: 78
|
||||
on_y:
|
||||
self.another = 23
|
||||
self.home = 78
|
||||
canvas.before:
|
||||
Color:
|
||||
rgb: 1, 1, 1
|
||||
|
||||
Widget:
|
||||
size: 55, self.y + 10
|
||||
SomeWidget:
|
||||
size_hint_x: .5
|
|
@ -0,0 +1,2 @@
|
|||
[section]
|
||||
key=value
|
169
kivy_venv/lib/python3.11/site-packages/kivy/tests/fixtures.py
Normal file
169
kivy_venv/lib/python3.11/site-packages/kivy/tests/fixtures.py
Normal file
|
@ -0,0 +1,169 @@
|
|||
import pytest
|
||||
import gc
|
||||
import weakref
|
||||
import time
|
||||
import os.path
|
||||
|
||||
__all__ = ('kivy_clock', 'kivy_metrics', 'kivy_exception_manager', 'kivy_app')
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def kivy_clock():
|
||||
from kivy.context import Context
|
||||
from kivy.clock import ClockBase
|
||||
|
||||
context = Context(init=False)
|
||||
context['Clock'] = ClockBase()
|
||||
context.push()
|
||||
|
||||
from kivy.clock import Clock
|
||||
Clock._max_fps = 0
|
||||
|
||||
try:
|
||||
Clock.start_clock()
|
||||
yield Clock
|
||||
Clock.stop_clock()
|
||||
finally:
|
||||
context.pop()
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def kivy_metrics():
|
||||
from kivy.context import Context
|
||||
from kivy.metrics import MetricsBase, Metrics
|
||||
from kivy._metrics import dispatch_pixel_scale
|
||||
|
||||
context = Context(init=False)
|
||||
context['Metrics'] = MetricsBase()
|
||||
context.push()
|
||||
# need to do it to reset the global value
|
||||
dispatch_pixel_scale()
|
||||
|
||||
try:
|
||||
yield Metrics
|
||||
finally:
|
||||
context.pop()
|
||||
Metrics._set_cached_scaling()
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def kivy_exception_manager():
|
||||
from kivy.context import Context
|
||||
from kivy.base import ExceptionManagerBase, ExceptionManager
|
||||
|
||||
context = Context(init=False)
|
||||
context['ExceptionManager'] = ExceptionManagerBase()
|
||||
context.push()
|
||||
|
||||
try:
|
||||
yield ExceptionManager
|
||||
finally:
|
||||
context.pop()
|
||||
|
||||
|
||||
# keep track of all the kivy app fixtures so that we can check that it
|
||||
# properly dies
|
||||
apps = []
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
async def kivy_app(request, nursery):
|
||||
gc.collect()
|
||||
if apps:
|
||||
last_app, last_request = apps.pop()
|
||||
assert last_app() is None, \
|
||||
'Memory leak: failed to release app for test ' + repr(last_request)
|
||||
|
||||
from os import environ
|
||||
environ['KIVY_USE_DEFAULTCONFIG'] = '1'
|
||||
|
||||
# force window size + remove all inputs
|
||||
from kivy.config import Config
|
||||
Config.set('graphics', 'width', '320')
|
||||
Config.set('graphics', 'height', '240')
|
||||
for items in Config.items('input'):
|
||||
Config.remove_option('input', items[0])
|
||||
|
||||
from kivy.core.window import Window
|
||||
from kivy.context import Context
|
||||
from kivy.clock import ClockBase
|
||||
from kivy.factory import FactoryBase, Factory
|
||||
from kivy.app import App
|
||||
from kivy.lang.builder import BuilderBase, Builder
|
||||
from kivy.base import stopTouchApp
|
||||
from kivy import kivy_data_dir
|
||||
from kivy.logger import LoggerHistory
|
||||
|
||||
kivy_eventloop = environ.get('KIVY_EVENTLOOP', 'asyncio')
|
||||
if kivy_eventloop == 'asyncio':
|
||||
pytest.importorskip(
|
||||
'pytest_asyncio',
|
||||
reason='KIVY_EVENTLOOP == "asyncio" but '
|
||||
'"pytest_asyncio" is not installed')
|
||||
async_lib = 'asyncio'
|
||||
elif kivy_eventloop == 'trio':
|
||||
pytest.importorskip(
|
||||
'pytest_trio',
|
||||
reason='KIVY_EVENTLOOP == "trio" but '
|
||||
'"pytest_trio" is not installed')
|
||||
async_lib = 'trio'
|
||||
else:
|
||||
pytest.skip(
|
||||
'KIVY_EVENTLOOP must be set to either of "asyncio" or '
|
||||
'"trio" to run async tests')
|
||||
|
||||
context = Context(init=False)
|
||||
context['Clock'] = ClockBase(async_lib=async_lib)
|
||||
|
||||
# have to make sure all global kv files are loaded before this because
|
||||
# globally read kv files (e.g. on module import) will not be loaded again
|
||||
# in the new builder, except if manually loaded, which we don't do
|
||||
context['Factory'] = FactoryBase.create_from(Factory)
|
||||
context['Builder'] = BuilderBase.create_from(Builder)
|
||||
context.push()
|
||||
|
||||
Window.create_window()
|
||||
Window.register()
|
||||
Window.initialized = True
|
||||
Window.canvas.clear()
|
||||
|
||||
app = request.param[0]()
|
||||
app.set_async_lib(async_lib)
|
||||
|
||||
if async_lib == 'asyncio':
|
||||
import asyncio
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.create_task(app.async_run())
|
||||
else:
|
||||
nursery.start_soon(app.async_run)
|
||||
from kivy.clock import Clock
|
||||
Clock._max_fps = 0
|
||||
|
||||
ts = time.perf_counter()
|
||||
while not app.app_has_started:
|
||||
await app.async_sleep(.1)
|
||||
if time.perf_counter() - ts >= 10:
|
||||
raise TimeoutError()
|
||||
|
||||
await app.wait_clock_frames(5)
|
||||
|
||||
yield app
|
||||
|
||||
stopTouchApp()
|
||||
|
||||
ts = time.perf_counter()
|
||||
while not app.app_has_stopped:
|
||||
await app.async_sleep(.1)
|
||||
if time.perf_counter() - ts >= 10:
|
||||
raise TimeoutError()
|
||||
|
||||
for child in Window.children[:]:
|
||||
Window.remove_widget(child)
|
||||
context.pop()
|
||||
|
||||
# release all the resources
|
||||
del context
|
||||
LoggerHistory.clear_history()
|
||||
apps.append((weakref.ref(app), request))
|
||||
del app
|
||||
gc.collect()
|
|
@ -0,0 +1,195 @@
|
|||
from kivy.app import App
|
||||
from kivy.uix.floatlayout import FloatLayout
|
||||
from kivy.lang import Builder
|
||||
from kivy.resources import resource_find
|
||||
from kivy.clock import Clock
|
||||
|
||||
import timeit
|
||||
|
||||
Builder.load_string('''
|
||||
<PerfApp>:
|
||||
value: 0
|
||||
but: but.__self__
|
||||
slider: slider
|
||||
text_input: text_input
|
||||
BoxLayout:
|
||||
orientation: 'vertical'
|
||||
TextInput:
|
||||
id: text_input
|
||||
BoxLayout:
|
||||
orientation: 'vertical'
|
||||
size_hint: 1, .2
|
||||
BoxLayout:
|
||||
Button:
|
||||
id: but
|
||||
text: 'Start Test'
|
||||
on_release: root.start_test() if self.text == 'Start Test'\
|
||||
else ''
|
||||
Slider:
|
||||
id: slider
|
||||
min: 0
|
||||
max: 100
|
||||
value: root.value
|
||||
''')
|
||||
|
||||
|
||||
class PerfApp(App, FloatLayout):
|
||||
|
||||
def build(self):
|
||||
return self
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(PerfApp, self).__init__(**kwargs)
|
||||
self.tests = []
|
||||
tests = (self.load_large_text, self.stress_insert,
|
||||
self.stress_del, self.stress_selection)
|
||||
for test in tests:
|
||||
but = type(self.but)(text=test.__name__)
|
||||
self.but.parent.add_widget(but)
|
||||
but.test = test
|
||||
self.tests.append(but)
|
||||
self.test_done = True
|
||||
|
||||
def load_large_text(self, *largs):
|
||||
print('loading uix/textinput.py....')
|
||||
self.test_done = False
|
||||
fd = open(resource_find('uix/textinput.py'), 'r')
|
||||
print('putting text in textinput')
|
||||
|
||||
def load_text(*l):
|
||||
self.text_input.text = fd.read()
|
||||
|
||||
t = timeit.Timer(load_text)
|
||||
ttk = t.timeit(1)
|
||||
fd.close()
|
||||
import resource
|
||||
print('mem usage after test')
|
||||
print(resource.getrusage(resource.RUSAGE_SELF).ru_maxrss / 1024, 'MB')
|
||||
print('------------------------------------------')
|
||||
print('Loaded', len(self.text_input._lines), 'lines', ttk, 'secs')
|
||||
print('------------------------------------------')
|
||||
self.test_done = True
|
||||
|
||||
def stress_del(self, *largs):
|
||||
self.test_done = False
|
||||
text_input = self.text_input
|
||||
self.lt = len_text = len(text_input.text)
|
||||
target = len_text - (210 * 9)
|
||||
self.tot_time = 0
|
||||
ev = None
|
||||
|
||||
def dlt(*l):
|
||||
if len(text_input.text) <= target:
|
||||
ev.cancel()
|
||||
print('Done!')
|
||||
m_len = len(text_input._lines)
|
||||
print('deleted 210 characters 9 times')
|
||||
import resource
|
||||
print('mem usage after test')
|
||||
print(resource.getrusage(resource.RUSAGE_SELF).ru_maxrss /
|
||||
1024, 'MB')
|
||||
print('total lines in text input:', m_len)
|
||||
print('--------------------------------------')
|
||||
print('total time elapsed:', self.tot_time)
|
||||
print('--------------------------------------')
|
||||
self.test_done = True
|
||||
return
|
||||
text_input.select_text(self.lt - 220, self.lt - 10)
|
||||
text_input.delete_selection()
|
||||
self.lt -= 210
|
||||
text_input.scroll_y -= 100
|
||||
self.tot_time += l[0]
|
||||
ev()
|
||||
ev = Clock.create_trigger(dlt)
|
||||
ev()
|
||||
|
||||
def stress_insert(self, *largs):
|
||||
self.test_done = False
|
||||
text_input = self.text_input
|
||||
text_input.select_all()
|
||||
text_input.copy(text_input.selection_text)
|
||||
text_input.cursor = text_input.get_cursor_from_index(
|
||||
text_input.selection_to)
|
||||
len_text = len(text_input._lines)
|
||||
self.tot_time = 0
|
||||
ev = None
|
||||
|
||||
def pste(*l):
|
||||
if len(text_input._lines) >= (len_text) * 9:
|
||||
ev.cancel()
|
||||
print('Done!')
|
||||
m_len = len(text_input._lines)
|
||||
print('pasted', len_text, 'lines',
|
||||
round((m_len - len_text) / len_text), 'times')
|
||||
import resource
|
||||
print('mem usage after test')
|
||||
print(resource.getrusage(resource.RUSAGE_SELF).ru_maxrss /
|
||||
1024, 'MB')
|
||||
print('total lines in text input:', m_len)
|
||||
print('--------------------------------------')
|
||||
print('total time elapsed:', self.tot_time)
|
||||
print('--------------------------------------')
|
||||
self.test_done = True
|
||||
return
|
||||
self.tot_time += l[0]
|
||||
text_input.paste()
|
||||
ev()
|
||||
ev = Clock.create_trigger(pste)
|
||||
ev()
|
||||
|
||||
def stress_selection(self, *largs):
|
||||
self.test_done = False
|
||||
text_input = self.text_input
|
||||
self.tot_time = 0
|
||||
old_selection_from = text_input.selection_from - 210
|
||||
ev = None
|
||||
|
||||
def pste(*l):
|
||||
if text_input.selection_from >= old_selection_from:
|
||||
ev.cancel()
|
||||
print('Done!')
|
||||
import resource
|
||||
print('mem usage after test')
|
||||
print(resource.getrusage(resource.RUSAGE_SELF).ru_maxrss /
|
||||
1024, 'MB')
|
||||
print('--------------------------------------')
|
||||
print('total time elapsed:', self.tot_time)
|
||||
print('--------------------------------------')
|
||||
self.test_done = True
|
||||
return
|
||||
text_input.select_text(text_input.selection_from - 1,
|
||||
text_input.selection_to)
|
||||
ev()
|
||||
ev = Clock.create_trigger(pste)
|
||||
ev()
|
||||
|
||||
def start_test(self, *largs):
|
||||
self.but.text = 'test started'
|
||||
self.slider.max = len(self.tests)
|
||||
ev = None
|
||||
|
||||
def test(*l):
|
||||
if self.test_done:
|
||||
try:
|
||||
but = self.tests[int(self.slider.value)]
|
||||
self.slider.value += 1
|
||||
but.state = 'down'
|
||||
print('=====================')
|
||||
print('Test:', but.text)
|
||||
print('=====================')
|
||||
but.test(but)
|
||||
except IndexError:
|
||||
for but in self.tests:
|
||||
but.state = 'normal'
|
||||
self.but.text = 'Start Test'
|
||||
self.slider.value = 0
|
||||
print('===================')
|
||||
print('All Tests Completed')
|
||||
print('===================')
|
||||
ev.cancel()
|
||||
|
||||
ev = Clock.schedule_interval(test, 1)
|
||||
|
||||
|
||||
if __name__ in ('__main__', ):
|
||||
PerfApp().run()
|
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,12 @@
|
|||
|
||||
|
||||
from project.widget import MyWidget
|
||||
|
||||
if __name__ == '__main__':
|
||||
w = MyWidget()
|
||||
|
||||
assert w.x == w.y
|
||||
w.y = 868
|
||||
assert w.x == 868
|
||||
w.y = 370
|
||||
assert w.x == 370
|
|
@ -0,0 +1,37 @@
|
|||
# -*- mode: python -*-
|
||||
|
||||
block_cipher = None
|
||||
from kivy_deps import sdl2, glew
|
||||
from kivy.tools.packaging.pyinstaller_hooks import runtime_hooks, hookspath
|
||||
import os
|
||||
|
||||
|
||||
a = Analysis(['main.py'],
|
||||
pathex=[os.environ['__KIVY_PYINSTALLER_DIR']],
|
||||
binaries=[],
|
||||
datas=[],
|
||||
hiddenimports=[],
|
||||
hookspath=[os.environ['__KIVY_PYINSTALLER_DIR']],
|
||||
runtime_hooks=runtime_hooks(),
|
||||
excludes=['numpy', 'ffpyplayer'],
|
||||
win_no_prefer_redirects=False,
|
||||
win_private_assemblies=False,
|
||||
cipher=block_cipher)
|
||||
pyz = PYZ(a.pure, a.zipped_data,
|
||||
cipher=block_cipher)
|
||||
exe = EXE(pyz,
|
||||
a.scripts,
|
||||
exclude_binaries=True,
|
||||
name='main',
|
||||
debug=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
console=True )
|
||||
coll = COLLECT(exe,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
*[Tree(p) for p in (sdl2.dep_bins + glew.dep_bins)],
|
||||
strip=False,
|
||||
upx=True,
|
||||
name='main')
|
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,12 @@
|
|||
from kivy.uix.widget import Widget
|
||||
|
||||
|
||||
class MyWidget(Widget):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(MyWidget, self).__init__(**kwargs)
|
||||
|
||||
def callback(*l):
|
||||
self.x = self.y
|
||||
self.fbind('y', callback)
|
||||
callback()
|
|
@ -0,0 +1,115 @@
|
|||
import pytest
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import shutil
|
||||
|
||||
if sys.platform != 'win32':
|
||||
pytestmark = pytest.mark.skip(
|
||||
"PyInstaller is currently only tested on Windows")
|
||||
else:
|
||||
try:
|
||||
import PyInstaller
|
||||
except ImportError:
|
||||
pytestmark = pytest.mark.skip("PyInstaller is not available")
|
||||
|
||||
|
||||
@pytest.mark.incremental
|
||||
class PyinstallerBase(object):
|
||||
|
||||
pinstall_path = ''
|
||||
|
||||
env = None
|
||||
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
cls.env = cls.get_env()
|
||||
|
||||
@classmethod
|
||||
def get_env(cls):
|
||||
env = os.environ.copy()
|
||||
env['__KIVY_PYINSTALLER_DIR'] = cls.pinstall_path
|
||||
|
||||
if 'PYTHONPATH' not in env:
|
||||
env['PYTHONPATH'] = cls.pinstall_path
|
||||
else:
|
||||
env['PYTHONPATH'] = cls.pinstall_path + os.sep + env['PYTHONPATH']
|
||||
return env
|
||||
|
||||
@classmethod
|
||||
def get_run_env(cls):
|
||||
return os.environ.copy()
|
||||
|
||||
def test_project(self):
|
||||
try:
|
||||
# check that the project works normally before packaging
|
||||
subprocess.check_output(
|
||||
[sys.executable or 'python',
|
||||
os.path.join(self.pinstall_path, 'main.py')],
|
||||
stderr=subprocess.STDOUT, env=self.env)
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(e.output.decode('utf8'))
|
||||
raise
|
||||
|
||||
def test_packaging(self):
|
||||
dist = os.path.join(self.pinstall_path, 'dist')
|
||||
build = os.path.join(self.pinstall_path, 'build')
|
||||
try:
|
||||
# create pyinstaller package
|
||||
subprocess.check_output(
|
||||
[sys.executable or 'python', '-m', 'PyInstaller',
|
||||
os.path.join(self.pinstall_path, 'main.spec'),
|
||||
'--distpath', dist, '--workpath', build],
|
||||
stderr=subprocess.STDOUT, env=self.env)
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(e.output.decode('utf8'))
|
||||
raise
|
||||
|
||||
def test_packaged_project(self):
|
||||
try:
|
||||
# test package
|
||||
subprocess.check_output(
|
||||
os.path.join(self.pinstall_path, 'dist', 'main', 'main'),
|
||||
stderr=subprocess.STDOUT, env=self.get_run_env())
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(e.output.decode('utf8'))
|
||||
raise
|
||||
|
||||
@classmethod
|
||||
def teardown_class(cls):
|
||||
shutil.rmtree(
|
||||
os.path.join(cls.pinstall_path, '__pycache__'),
|
||||
ignore_errors=True)
|
||||
shutil.rmtree(
|
||||
os.path.join(cls.pinstall_path, 'build'), ignore_errors=True)
|
||||
shutil.rmtree(
|
||||
os.path.join(cls.pinstall_path, 'dist'), ignore_errors=True)
|
||||
shutil.rmtree(
|
||||
os.path.join(cls.pinstall_path, 'project', '__pycache__'),
|
||||
ignore_errors=True)
|
||||
|
||||
|
||||
class TestSimpleWidget(PyinstallerBase):
|
||||
|
||||
pinstall_path = os.path.join(os.path.dirname(__file__), 'simple_widget')
|
||||
|
||||
|
||||
class TestVideoWidget(PyinstallerBase):
|
||||
|
||||
pinstall_path = os.path.join(os.path.dirname(__file__), 'video_widget')
|
||||
|
||||
@classmethod
|
||||
def get_env(cls):
|
||||
env = super(TestVideoWidget, cls).get_env()
|
||||
import kivy
|
||||
env['__KIVY_VIDEO_TEST_FNAME'] = os.path.abspath(os.path.join(
|
||||
kivy.kivy_examples_dir, "widgets", "cityCC0.mpg"))
|
||||
return env
|
||||
|
||||
@classmethod
|
||||
def get_run_env(cls):
|
||||
env = super(TestVideoWidget, cls).get_run_env()
|
||||
import kivy
|
||||
env['__KIVY_VIDEO_TEST_FNAME'] = os.path.abspath(os.path.join(
|
||||
kivy.kivy_examples_dir, "widgets", "cityCC0.mpg"))
|
||||
return env
|
Binary file not shown.
|
@ -0,0 +1,6 @@
|
|||
from project import VideoApp
|
||||
|
||||
if __name__ == '__main__':
|
||||
from kivy.core.video import Video
|
||||
assert Video is not None
|
||||
VideoApp().run()
|
|
@ -0,0 +1,50 @@
|
|||
# -*- mode: python -*-
|
||||
|
||||
block_cipher = None
|
||||
from kivy_deps import sdl2, glew
|
||||
from kivy.tools.packaging.pyinstaller_hooks import runtime_hooks, hookspath
|
||||
import os
|
||||
|
||||
deps = list(sdl2.dep_bins + glew.dep_bins)
|
||||
try:
|
||||
import ffpyplayer
|
||||
deps.extend(ffpyplayer.dep_bins)
|
||||
except ImportError:
|
||||
pass
|
||||
try:
|
||||
from kivy_deps import gstreamer
|
||||
deps.extend(gstreamer.dep_bins)
|
||||
except ImportError:
|
||||
pass
|
||||
print('deps are: ', deps)
|
||||
|
||||
|
||||
a = Analysis(['main.py'],
|
||||
pathex=[os.environ['__KIVY_PYINSTALLER_DIR']],
|
||||
binaries=[],
|
||||
datas=[],
|
||||
hiddenimports=[],
|
||||
hookspath=[os.environ['__KIVY_PYINSTALLER_DIR']],
|
||||
runtime_hooks=runtime_hooks(),
|
||||
excludes=['numpy',],
|
||||
win_no_prefer_redirects=False,
|
||||
win_private_assemblies=False,
|
||||
cipher=block_cipher)
|
||||
pyz = PYZ(a.pure, a.zipped_data,
|
||||
cipher=block_cipher)
|
||||
exe = EXE(pyz,
|
||||
a.scripts,
|
||||
exclude_binaries=True,
|
||||
name='main',
|
||||
debug=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
console=True )
|
||||
coll = COLLECT(exe,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
*[Tree(p) for p in deps],
|
||||
strip=False,
|
||||
upx=True,
|
||||
name='main')
|
|
@ -0,0 +1,38 @@
|
|||
from kivy.app import App
|
||||
from kivy.uix.videoplayer import VideoPlayer
|
||||
from kivy.clock import Clock
|
||||
import os
|
||||
import time
|
||||
|
||||
|
||||
class VideoApp(App):
|
||||
|
||||
player = None
|
||||
|
||||
start_t = None
|
||||
|
||||
def build(self):
|
||||
self.player = player = VideoPlayer(
|
||||
source=os.environ['__KIVY_VIDEO_TEST_FNAME'], volume=0)
|
||||
|
||||
self.player.fbind('position', self.check_position)
|
||||
Clock.schedule_once(self.start_player, 0)
|
||||
Clock.schedule_interval(self.stop_player, 1)
|
||||
return player
|
||||
|
||||
def start_player(self, *args):
|
||||
self.player.state = 'play'
|
||||
self.start_t = time.perf_counter()
|
||||
|
||||
def check_position(self, *args):
|
||||
if self.player.position > 0.1:
|
||||
self.stop_player()
|
||||
|
||||
def stop_player(self, *args):
|
||||
if time.perf_counter() - self.start_t > 10:
|
||||
assert self.player.duration > 0
|
||||
assert self.player.position > 0
|
||||
self.stop()
|
||||
else:
|
||||
if self.player.position > 0 and self.player.duration > 0:
|
||||
self.stop()
|
Binary file not shown.
|
@ -0,0 +1,6 @@
|
|||
[pytest]
|
||||
markers =
|
||||
logmodepython: mark a test for the logger module in python mode
|
||||
logmodemixed: mark a test for the logger module in mixed mode
|
||||
incremental: mark a test as incremental
|
||||
|
BIN
kivy_venv/lib/python3.11/site-packages/kivy/tests/sample1.ogg
Normal file
BIN
kivy_venv/lib/python3.11/site-packages/kivy/tests/sample1.ogg
Normal file
Binary file not shown.
|
@ -0,0 +1,415 @@
|
|||
'''
|
||||
Animations tests
|
||||
================
|
||||
'''
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture(scope='module')
|
||||
def ec_cls():
|
||||
class EventCounter:
|
||||
def __init__(self, anim):
|
||||
self.n_start = 0
|
||||
self.n_progress = 0
|
||||
self.n_complete = 0
|
||||
anim.bind(on_start=self.on_start,
|
||||
on_progress=self.on_progress,
|
||||
on_complete=self.on_complete)
|
||||
|
||||
def on_start(self, anim, widget):
|
||||
self.n_start += 1
|
||||
|
||||
def on_progress(self, anim, widget, progress):
|
||||
self.n_progress += 1
|
||||
|
||||
def on_complete(self, anim, widget):
|
||||
self.n_complete += 1
|
||||
|
||||
def assert_(self, n_start, n_progress_greater_than_zero, n_complete):
|
||||
assert self.n_start == n_start
|
||||
if n_progress_greater_than_zero:
|
||||
assert self.n_progress > 0
|
||||
else:
|
||||
assert self.n_progress == 0
|
||||
assert self.n_complete == n_complete
|
||||
return EventCounter
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def cleanup():
|
||||
from kivy.animation import Animation
|
||||
Animation.cancel_all(None)
|
||||
|
||||
|
||||
def no_animations_being_played():
|
||||
from kivy.animation import Animation
|
||||
return len(Animation._instances) == 0
|
||||
|
||||
|
||||
def sleep(t):
|
||||
from time import time, sleep
|
||||
from kivy.clock import Clock
|
||||
tick = Clock.tick
|
||||
deadline = time() + t
|
||||
while time() < deadline:
|
||||
sleep(.01)
|
||||
tick()
|
||||
|
||||
|
||||
class TestAnimation:
|
||||
|
||||
def test_start_animation(self):
|
||||
from kivy.animation import Animation
|
||||
from kivy.uix.widget import Widget
|
||||
a = Animation(x=100, d=1)
|
||||
w = Widget()
|
||||
a.start(w)
|
||||
sleep(1.5)
|
||||
assert w.x == pytest.approx(100)
|
||||
assert no_animations_being_played()
|
||||
|
||||
def test_animation_duration_0(self):
|
||||
from kivy.animation import Animation
|
||||
from kivy.uix.widget import Widget
|
||||
a = Animation(x=100, d=0)
|
||||
w = Widget()
|
||||
a.start(w)
|
||||
sleep(.5)
|
||||
assert no_animations_being_played()
|
||||
|
||||
def test_cancel_all(self):
|
||||
from kivy.animation import Animation
|
||||
from kivy.uix.widget import Widget
|
||||
a1 = Animation(x=100)
|
||||
a2 = Animation(y=100)
|
||||
w1 = Widget()
|
||||
w2 = Widget()
|
||||
a1.start(w1)
|
||||
a1.start(w2)
|
||||
a2.start(w1)
|
||||
a2.start(w2)
|
||||
assert not no_animations_being_played()
|
||||
Animation.cancel_all(None)
|
||||
assert no_animations_being_played()
|
||||
|
||||
def test_cancel_all_2(self):
|
||||
from kivy.animation import Animation
|
||||
from kivy.uix.widget import Widget
|
||||
a1 = Animation(x=100)
|
||||
a2 = Animation(y=100)
|
||||
w1 = Widget()
|
||||
w2 = Widget()
|
||||
a1.start(w1)
|
||||
a1.start(w2)
|
||||
a2.start(w1)
|
||||
a2.start(w2)
|
||||
assert not no_animations_being_played()
|
||||
Animation.cancel_all(None, 'x', 'z')
|
||||
assert not no_animations_being_played()
|
||||
Animation.cancel_all(None, 'y')
|
||||
assert no_animations_being_played()
|
||||
|
||||
def test_stop_animation(self):
|
||||
from kivy.animation import Animation
|
||||
from kivy.uix.widget import Widget
|
||||
a = Animation(x=100, d=1)
|
||||
w = Widget()
|
||||
a.start(w)
|
||||
sleep(.5)
|
||||
a.stop(w)
|
||||
assert w.x != pytest.approx(100)
|
||||
assert w.x != pytest.approx(0)
|
||||
assert no_animations_being_played()
|
||||
|
||||
def test_stop_all(self):
|
||||
from kivy.animation import Animation
|
||||
from kivy.uix.widget import Widget
|
||||
a = Animation(x=100, d=1)
|
||||
w = Widget()
|
||||
a.start(w)
|
||||
sleep(.5)
|
||||
Animation.stop_all(w)
|
||||
assert no_animations_being_played()
|
||||
|
||||
def test_stop_all_2(self):
|
||||
from kivy.animation import Animation
|
||||
from kivy.uix.widget import Widget
|
||||
a = Animation(x=100, d=1)
|
||||
w = Widget()
|
||||
a.start(w)
|
||||
sleep(.5)
|
||||
Animation.stop_all(w, 'x')
|
||||
assert no_animations_being_played()
|
||||
|
||||
def test_duration(self):
|
||||
from kivy.animation import Animation
|
||||
a = Animation(x=100, d=1)
|
||||
assert a.duration == 1
|
||||
|
||||
def test_transition(self):
|
||||
from kivy.animation import Animation, AnimationTransition
|
||||
a = Animation(x=100, t='out_bounce')
|
||||
assert a.transition is AnimationTransition.out_bounce
|
||||
|
||||
def test_animated_properties(self):
|
||||
from kivy.animation import Animation
|
||||
a = Animation(x=100)
|
||||
assert a.animated_properties == {'x': 100, }
|
||||
|
||||
def test_animated_instruction(self):
|
||||
from kivy.graphics import Scale
|
||||
from kivy.animation import Animation
|
||||
a = Animation(x=100, d=1)
|
||||
instruction = Scale(3, 3, 3)
|
||||
a.start(instruction)
|
||||
assert a.animated_properties == {'x': 100, }
|
||||
assert instruction.x == pytest.approx(3)
|
||||
sleep(1.5)
|
||||
assert instruction.x == pytest.approx(100)
|
||||
assert no_animations_being_played()
|
||||
|
||||
def test_weakref(self):
|
||||
import gc
|
||||
from kivy.animation import Animation
|
||||
from kivy.uix.widget import Widget
|
||||
w = Widget()
|
||||
a = Animation(x=100)
|
||||
a.start(w.proxy_ref)
|
||||
del w
|
||||
gc.collect()
|
||||
try:
|
||||
sleep(1.)
|
||||
except ReferenceError:
|
||||
pass
|
||||
assert no_animations_being_played()
|
||||
|
||||
|
||||
class TestSequence:
|
||||
|
||||
def test_cancel_all(self):
|
||||
from kivy.animation import Animation
|
||||
from kivy.uix.widget import Widget
|
||||
a = Animation(x=100) + Animation(x=0)
|
||||
w = Widget()
|
||||
a.start(w)
|
||||
sleep(.5)
|
||||
Animation.cancel_all(w)
|
||||
assert no_animations_being_played()
|
||||
|
||||
def test_cancel_all_2(self):
|
||||
from kivy.animation import Animation
|
||||
from kivy.uix.widget import Widget
|
||||
a = Animation(x=100) + Animation(x=0)
|
||||
w = Widget()
|
||||
a.start(w)
|
||||
sleep(.5)
|
||||
Animation.cancel_all(w, 'x')
|
||||
assert no_animations_being_played()
|
||||
|
||||
def test_stop_all(self):
|
||||
from kivy.animation import Animation
|
||||
from kivy.uix.widget import Widget
|
||||
a = Animation(x=100) + Animation(x=0)
|
||||
w = Widget()
|
||||
a.start(w)
|
||||
sleep(.5)
|
||||
Animation.stop_all(w)
|
||||
assert no_animations_being_played()
|
||||
|
||||
def test_stop_all_2(self):
|
||||
from kivy.animation import Animation
|
||||
from kivy.uix.widget import Widget
|
||||
a = Animation(x=100) + Animation(x=0)
|
||||
w = Widget()
|
||||
a.start(w)
|
||||
sleep(.5)
|
||||
Animation.stop_all(w, 'x')
|
||||
assert no_animations_being_played()
|
||||
|
||||
def test_count_events(self, ec_cls):
|
||||
from kivy.animation import Animation
|
||||
from kivy.uix.widget import Widget
|
||||
a = Animation(x=100, d=.5) + Animation(x=0, d=.5)
|
||||
w = Widget()
|
||||
ec = ec_cls(a)
|
||||
ec1 = ec_cls(a.anim1)
|
||||
ec2 = ec_cls(a.anim2)
|
||||
a.start(w)
|
||||
|
||||
# right after the animation starts
|
||||
ec.assert_(1, False, 0)
|
||||
ec1.assert_(1, False, 0)
|
||||
ec2.assert_(0, False, 0)
|
||||
sleep(.2)
|
||||
|
||||
# during the first half of the animation
|
||||
ec.assert_(1, True, 0)
|
||||
ec1.assert_(1, True, 0)
|
||||
ec2.assert_(0, False, 0)
|
||||
sleep(.5)
|
||||
|
||||
# during the second half of the animation
|
||||
ec.assert_(1, True, 0)
|
||||
ec1.assert_(1, True, 1)
|
||||
ec2.assert_(1, True, 0)
|
||||
sleep(.5)
|
||||
|
||||
# after the animation completed
|
||||
ec.assert_(1, True, 1)
|
||||
ec1.assert_(1, True, 1)
|
||||
ec2.assert_(1, True, 1)
|
||||
assert no_animations_being_played()
|
||||
|
||||
def test_have_properties_to_animate(self):
|
||||
from kivy.animation import Animation
|
||||
from kivy.uix.widget import Widget
|
||||
a = Animation(x=100) + Animation(x=0)
|
||||
w = Widget()
|
||||
assert not a.have_properties_to_animate(w)
|
||||
a.start(w)
|
||||
assert a.have_properties_to_animate(w)
|
||||
a.stop(w)
|
||||
assert not a.have_properties_to_animate(w)
|
||||
assert no_animations_being_played()
|
||||
|
||||
def test_animated_properties(self):
|
||||
from kivy.animation import Animation
|
||||
a = Animation(x=100, y=200) + Animation(x=0)
|
||||
assert a.animated_properties == {'x': 0, 'y': 200, }
|
||||
|
||||
def test_transition(self):
|
||||
from kivy.animation import Animation
|
||||
a = Animation(x=100) + Animation(x=0)
|
||||
with pytest.raises(AttributeError):
|
||||
a.transition
|
||||
|
||||
|
||||
class TestRepetitiveSequence:
|
||||
|
||||
def test_stop(self):
|
||||
from kivy.animation import Animation
|
||||
from kivy.uix.widget import Widget
|
||||
a = Animation(x=100) + Animation(x=0)
|
||||
a.repeat = True
|
||||
w = Widget()
|
||||
a.start(w)
|
||||
a.stop(w)
|
||||
assert no_animations_being_played()
|
||||
|
||||
def test_count_events(self, ec_cls):
|
||||
from kivy.animation import Animation
|
||||
from kivy.uix.widget import Widget
|
||||
a = Animation(x=100, d=.5) + Animation(x=0, d=.5)
|
||||
a.repeat = True
|
||||
w = Widget()
|
||||
ec = ec_cls(a)
|
||||
ec1 = ec_cls(a.anim1)
|
||||
ec2 = ec_cls(a.anim2)
|
||||
a.start(w)
|
||||
|
||||
# right after the animation starts
|
||||
ec.assert_(1, False, 0)
|
||||
ec1.assert_(1, False, 0)
|
||||
ec2.assert_(0, False, 0)
|
||||
sleep(.2)
|
||||
|
||||
# during the first half of the first round of the animation
|
||||
ec.assert_(1, True, 0)
|
||||
ec1.assert_(1, True, 0)
|
||||
ec2.assert_(0, False, 0)
|
||||
sleep(.5)
|
||||
|
||||
# during the second half of the first round of the animation
|
||||
ec.assert_(1, True, 0)
|
||||
ec1.assert_(1, True, 1)
|
||||
ec2.assert_(1, True, 0)
|
||||
sleep(.5)
|
||||
|
||||
# during the first half of the second round of the animation
|
||||
ec.assert_(1, True, 0)
|
||||
ec1.assert_(2, True, 1)
|
||||
ec2.assert_(1, True, 1)
|
||||
sleep(.5)
|
||||
|
||||
# during the second half of the second round of the animation
|
||||
ec.assert_(1, True, 0)
|
||||
ec1.assert_(2, True, 2)
|
||||
ec2.assert_(2, True, 1)
|
||||
a.stop(w)
|
||||
|
||||
# after the animation stopped
|
||||
ec.assert_(1, True, 1)
|
||||
ec1.assert_(2, True, 2)
|
||||
ec2.assert_(2, True, 2)
|
||||
assert no_animations_being_played()
|
||||
|
||||
|
||||
class TestParallel:
|
||||
|
||||
def test_have_properties_to_animate(self):
|
||||
from kivy.animation import Animation
|
||||
from kivy.uix.widget import Widget
|
||||
a = Animation(x=100) & Animation(y=100)
|
||||
w = Widget()
|
||||
assert not a.have_properties_to_animate(w)
|
||||
a.start(w)
|
||||
assert a.have_properties_to_animate(w)
|
||||
a.stop(w)
|
||||
assert not a.have_properties_to_animate(w)
|
||||
assert no_animations_being_played()
|
||||
|
||||
def test_cancel_property(self):
|
||||
from kivy.animation import Animation
|
||||
from kivy.uix.widget import Widget
|
||||
a = Animation(x=100) & Animation(y=100)
|
||||
w = Widget()
|
||||
a.start(w)
|
||||
a.cancel_property(w, 'x')
|
||||
assert not no_animations_being_played()
|
||||
a.stop(w)
|
||||
assert no_animations_being_played()
|
||||
|
||||
def test_animated_properties(self):
|
||||
from kivy.animation import Animation
|
||||
a = Animation(x=100) & Animation(y=100)
|
||||
assert a.animated_properties == {'x': 100, 'y': 100, }
|
||||
|
||||
def test_transition(self):
|
||||
from kivy.animation import Animation
|
||||
a = Animation(x=100) & Animation(y=100)
|
||||
with pytest.raises(AttributeError):
|
||||
a.transition
|
||||
|
||||
def test_count_events(self, ec_cls):
|
||||
from kivy.animation import Animation
|
||||
from kivy.uix.widget import Widget
|
||||
a = Animation(x=100) & Animation(y=100, d=.5)
|
||||
w = Widget()
|
||||
ec = ec_cls(a)
|
||||
ec1 = ec_cls(a.anim1)
|
||||
ec2 = ec_cls(a.anim2)
|
||||
a.start(w)
|
||||
|
||||
# right after the animation started
|
||||
ec.assert_(1, False, 0)
|
||||
ec1.assert_(1, False, 0)
|
||||
ec2.assert_(1, False, 0)
|
||||
sleep(.2)
|
||||
|
||||
# during the first half of the animation
|
||||
ec.assert_(1, False, 0) # n_progress is still 0 !!
|
||||
ec1.assert_(1, True, 0)
|
||||
ec2.assert_(1, True, 0)
|
||||
sleep(.5)
|
||||
|
||||
# during the second half of the animation
|
||||
ec.assert_(1, False, 0) # n_progress is still 0 !!
|
||||
ec1.assert_(1, True, 0)
|
||||
ec2.assert_(1, True, 1)
|
||||
sleep(.5)
|
||||
|
||||
# after the animation compeleted
|
||||
ec.assert_(1, False, 1) # n_progress is still 0 !
|
||||
ec1.assert_(1, True, 1)
|
||||
ec2.assert_(1, True, 1)
|
||||
assert no_animations_being_played()
|
216
kivy_venv/lib/python3.11/site-packages/kivy/tests/test_app.py
Normal file
216
kivy_venv/lib/python3.11/site-packages/kivy/tests/test_app.py
Normal file
|
@ -0,0 +1,216 @@
|
|||
from os import name
|
||||
import os.path
|
||||
from math import isclose
|
||||
from textwrap import dedent
|
||||
|
||||
from kivy.app import App
|
||||
from kivy.clock import Clock
|
||||
from kivy import lang
|
||||
from kivy.tests import GraphicUnitTest, async_run, UnitKivyApp
|
||||
|
||||
|
||||
class AppTest(GraphicUnitTest):
|
||||
def test_start_raw_app(self):
|
||||
lang._delayed_start = None
|
||||
a = App()
|
||||
Clock.schedule_once(a.stop, .1)
|
||||
a.run()
|
||||
|
||||
def test_start_app_with_kv(self):
|
||||
class TestKvApp(App):
|
||||
pass
|
||||
|
||||
lang._delayed_start = None
|
||||
a = TestKvApp()
|
||||
Clock.schedule_once(a.stop, .1)
|
||||
a.run()
|
||||
|
||||
def test_user_data_dir(self):
|
||||
a = App()
|
||||
data_dir = a.user_data_dir
|
||||
assert os.path.exists(data_dir)
|
||||
|
||||
def test_directory(self):
|
||||
a = App()
|
||||
assert os.path.exists(a.directory)
|
||||
|
||||
def test_name(self):
|
||||
class NameTest(App):
|
||||
pass
|
||||
|
||||
a = NameTest()
|
||||
assert a.name == 'nametest'
|
||||
|
||||
|
||||
def basic_app():
|
||||
from kivy.app import App
|
||||
from kivy.uix.label import Label
|
||||
|
||||
class TestApp(UnitKivyApp, App):
|
||||
def build(self):
|
||||
return Label(text='Hello, World!')
|
||||
|
||||
return TestApp()
|
||||
|
||||
|
||||
@async_run(app_cls_func=basic_app)
|
||||
async def test_basic_app(kivy_app):
|
||||
assert kivy_app.root.text == 'Hello, World!'
|
||||
|
||||
|
||||
def button_app():
|
||||
from kivy.app import App
|
||||
from kivy.uix.togglebutton import ToggleButton
|
||||
|
||||
class TestApp(UnitKivyApp, App):
|
||||
def build(self):
|
||||
return ToggleButton(text='Hello, World!')
|
||||
|
||||
return TestApp()
|
||||
|
||||
|
||||
@async_run(app_cls_func=button_app)
|
||||
async def test_button_app(kivy_app):
|
||||
assert kivy_app.root.text == 'Hello, World!'
|
||||
assert kivy_app.root.state == 'normal'
|
||||
|
||||
async for state, touch_pos in kivy_app.do_touch_down_up(
|
||||
widget=kivy_app.root, widget_jitter=True):
|
||||
pass
|
||||
|
||||
assert kivy_app.root.state == 'down'
|
||||
|
||||
|
||||
def scatter_app():
|
||||
from kivy.app import App
|
||||
from kivy.uix.label import Label
|
||||
from kivy.uix.scatter import Scatter
|
||||
|
||||
class TestApp(UnitKivyApp, App):
|
||||
def build(self):
|
||||
label = Label(text='Hello, World!', size=('200dp', '200dp'))
|
||||
scatter = Scatter(do_scale=False, do_rotation=False)
|
||||
scatter.add_widget(label)
|
||||
return scatter
|
||||
|
||||
return TestApp()
|
||||
|
||||
|
||||
@async_run(app_cls_func=scatter_app)
|
||||
async def test_drag_app(kivy_app):
|
||||
scatter = kivy_app.root
|
||||
assert tuple(scatter.pos) == (0, 0)
|
||||
|
||||
async for state, touch_pos in kivy_app.do_touch_drag(
|
||||
pos=(100, 100), target_pos=(200, 200)):
|
||||
pass
|
||||
|
||||
assert isclose(scatter.x, 100)
|
||||
assert isclose(scatter.y, 100)
|
||||
|
||||
|
||||
def text_app():
|
||||
from kivy.app import App
|
||||
from kivy.uix.textinput import TextInput
|
||||
|
||||
class TestApp(UnitKivyApp, App):
|
||||
def build(self):
|
||||
return TextInput()
|
||||
|
||||
return TestApp()
|
||||
|
||||
|
||||
@async_run(app_cls_func=text_app)
|
||||
async def test_text_app(kivy_app):
|
||||
text = kivy_app.root
|
||||
assert text.text == ''
|
||||
|
||||
# activate widget
|
||||
async for state, touch_pos in kivy_app.do_touch_down_up(widget=text):
|
||||
pass
|
||||
|
||||
async for state, value in kivy_app.do_keyboard_key(key='A', num_press=4):
|
||||
pass
|
||||
async for state, value in kivy_app.do_keyboard_key(key='q', num_press=3):
|
||||
pass
|
||||
|
||||
assert text.text == 'AAAAqqq'
|
||||
|
||||
|
||||
def graphics_app():
|
||||
from kivy.app import App
|
||||
from kivy.uix.widget import Widget
|
||||
from kivy.graphics import Color, Rectangle
|
||||
|
||||
class TestApp(UnitKivyApp, App):
|
||||
def build(self):
|
||||
widget = Widget()
|
||||
with widget.canvas:
|
||||
Color(1, 0, 0, 1)
|
||||
Rectangle(pos=(0, 0), size=(100, 100))
|
||||
Color(0, 1, 0, 1)
|
||||
Rectangle(pos=(100, 0), size=(100, 100))
|
||||
return widget
|
||||
|
||||
return TestApp()
|
||||
|
||||
|
||||
@async_run(app_cls_func=graphics_app)
|
||||
async def test_graphics_app(kivy_app):
|
||||
widget = kivy_app.root
|
||||
(r1, g1, b1, a1), (r2, g2, b2, a2) = kivy_app.get_widget_pos_pixel(
|
||||
widget, [(50, 50), (150, 50)])
|
||||
|
||||
assert not g1 and not b1 and not r2 and not b2
|
||||
assert r1 > 50 and a1 > 50 and g2 > 50 and a2 > 50
|
||||
|
||||
|
||||
def kv_app_ref_app():
|
||||
from kivy.app import App
|
||||
from kivy.lang import Builder
|
||||
from kivy.properties import ObjectProperty
|
||||
from kivy.uix.widget import Widget
|
||||
|
||||
class MyWidget(Widget):
|
||||
|
||||
obj = ObjectProperty(None)
|
||||
|
||||
Builder.load_string(dedent(
|
||||
"""
|
||||
<MyWidget>:
|
||||
obj: app.__self__
|
||||
"""))
|
||||
|
||||
class TestApp(UnitKivyApp, App):
|
||||
def build(self):
|
||||
return MyWidget()
|
||||
|
||||
return TestApp()
|
||||
|
||||
|
||||
@async_run(app_cls_func=kv_app_ref_app)
|
||||
async def test_leak_app_kv_property(kivy_app):
|
||||
# just tests whether the app is gc'd after the test is complete
|
||||
pass
|
||||
|
||||
|
||||
def kv_app_default_ref_app():
|
||||
from kivy.app import App
|
||||
from kivy.lang import Builder
|
||||
|
||||
class TestApp(UnitKivyApp, App):
|
||||
def build(self):
|
||||
# create property in kv and set app to it
|
||||
return Builder.load_string(dedent(
|
||||
"""
|
||||
Widget:
|
||||
obj: app.__self__
|
||||
"""))
|
||||
|
||||
return TestApp()
|
||||
|
||||
|
||||
@async_run(app_cls_func=kv_app_default_ref_app)
|
||||
async def test_leak_app_default_kv_property(kivy_app):
|
||||
# just tests whether the app is gc'd after the test is complete
|
||||
pass
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue