first commit

This commit is contained in:
Yura 2024-09-15 15:12:16 +03:00
commit 417e54da96
5696 changed files with 900003 additions and 0 deletions

View file

@ -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')

View file

@ -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)

View 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)

View file

@ -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)

View file

@ -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

View file

@ -0,0 +1,2 @@
[section]
key=value

View 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()

View file

@ -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()

View file

@ -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

View file

@ -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')

View file

@ -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()

View file

@ -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

View file

@ -0,0 +1,6 @@
from project import VideoApp
if __name__ == '__main__':
from kivy.core.video import Video
assert Video is not None
VideoApp().run()

View file

@ -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')

View file

@ -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()

View file

@ -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

View file

@ -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()

View 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