Skip to content

Commit 71eafe3

Browse files
committed
📦 Unify most of the test apps into one app
So depending on the supplied recipes, a different test app will be ran. The supported test apps are: - kivy - flask - no-gui
1 parent 65987a0 commit 71eafe3

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+1694
-1733
lines changed

testapps/on_device_unit_tests/buildozer.spec

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ package.domain = org.kivy
1313
source.dir = test_app
1414

1515
# (list) Source files to include (let empty to include all the files)
16-
source.include_exts = py,png,jpg,kv,atlas
16+
source.include_exts = py,png,jpg,kv,atlas,html,css,otf,txt
1717

1818
# (list) List of inclusions using pattern matching
1919
#source.include_patterns = assets/*,images/*.png
@@ -36,7 +36,7 @@ version = 0.1
3636

3737
# (list) Application requirements
3838
# comma separated e.g. requirements = sqlite3,kivy
39-
requirements = python3,kivy,openssl,numpy,sqlite3
39+
requirements = python3,kivy,libffi,openssl,numpy,sqlite3
4040

4141
# (str) Custom source folders for requirements
4242
# Sets custom source for any requirements with recipes
@@ -52,7 +52,7 @@ requirements = python3,kivy,openssl,numpy,sqlite3
5252
#icon.filename = %(source.dir)s/data/icon.png
5353

5454
# (str) Supported orientation (one of landscape, sensorLandscape, portrait or all)
55-
orientation = portrait
55+
orientation = all
5656

5757
# (list) List of service to declare
5858
#services = NAME:ENTRYPOINT_TO_PY,NAME2:ENTRYPOINT2_TO_PY
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
"""
2+
This is the `setup.py` file for the `on device unit test app`.
3+
4+
In this module we can control how will be built our test app. Depending on
5+
our requirements we can build an kivy, flask or a non-gui app. We default to an
6+
kivy app, since the python-for-android project its a sister project of kivy.
7+
8+
The parameter `requirements` it's crucial to determine the unit tests we will
9+
perform with our app, so we must explicitly name the recipe we want to test
10+
and, of course, we should have the proper test for the given recipe at
11+
`tests.test_requirements.py` or nothing will be tested. We control our default
12+
app requirements via the dictionary `options`. Here you have some examples
13+
to build the supported app modes::
14+
15+
- kivy *basic*: `sqlite3,libffi,openssl,pyjnius,kivy,python3,requests`
16+
- kivy *images/graphs*: `kivy,python3,numpy,matplotlib,Pillow`
17+
- kivy *encryption*: `kivy,python3,cryptography,pycryptodome,scrypt,
18+
m2crypto,pysha3`
19+
- flask (with webview bootstrap): `sqlite3,libffi,openssl,pyjnius,flask,
20+
python3,genericndkbuild`
21+
22+
23+
.. note:: just noting that, for the `kivy basic` app, we add the requirements:
24+
`sqlite3,libffi,openssl` so this way we will trigger the unit tests
25+
that we have for such recipes.
26+
27+
.. tip:: to force `python-for-android` generate an `flask` app without using
28+
the kwarg `bootstrap`, we add the recipe `genericndkbuild`, which will
29+
trigger the `webview bootstrap` at build time.
30+
"""
31+
32+
import os
33+
import sys
34+
from distutils.core import setup
35+
from setuptools import find_packages
36+
37+
# define a basic test app, which can be override passing the proper args to cli
38+
options = {
39+
'apk':
40+
{
41+
'requirements':
42+
'sqlite3,libffi,openssl,pyjnius,kivy,python3,requests',
43+
'android-api': 27,
44+
'ndk-api': 21,
45+
'dist-name': 'bdist_test_app_unittests',
46+
'arch': 'armeabi-v7a',
47+
'permissions': ['INTERNET', 'VIBRATE'],
48+
'orientation': 'sensor',
49+
'service': 'P4a_test_service:app_service.py',
50+
}
51+
}
52+
53+
# check if we overwrote the default test_app requirements via `cli`
54+
requirements = options['apk']['requirements'].rsplit(',')
55+
for n, arg in enumerate(sys.argv):
56+
if arg == '--requirements':
57+
print('found requirements')
58+
requirements = sys.argv[n + 1].rsplit(',')
59+
break
60+
61+
# write a file to let the test_app know which requirements we want to test
62+
# Note: later, when running the app, we will guess if we have the right test.
63+
app_requirements_txt = os.path.join(
64+
os.path.split(__file__)[0],
65+
'test_app',
66+
'app_requirements.txt',
67+
)
68+
with open(app_requirements_txt, 'w') as requirements_file:
69+
for req in requirements:
70+
requirements_file.write(f'{req.split("==")[0]}\n')
71+
72+
# run the install
73+
setup(
74+
name='test_app_unittests',
75+
version='1.1',
76+
description='p4a on device unit test app',
77+
author='Alexander Taylor, Pol Canelles',
78+
79+
packages=find_packages(),
80+
options=options,
81+
package_data={
82+
'test_app': ['*.py', '*.kv', '*.txt'],
83+
'test_app/static': ['*.png', '*.css', '*.otf'],
84+
'test_app/templates': ['*.html'],
85+
'test_app/tests': ['*.py'],
86+
}
87+
)
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
print('main.py was successfully called')
2+
print('this is the new main.py')
3+
4+
import sys
5+
print('python version is: ' + sys.version)
6+
print('python path is', sys.path)
7+
8+
import os
9+
print('imported os')
10+
print('contents of this dir', os.listdir('./'))
11+
12+
from flask import (
13+
Flask,
14+
render_template,
15+
request,
16+
Markup
17+
)
18+
19+
print('imported flask etc')
20+
21+
from constants import RUNNING_ON_ANDROID
22+
from tools import (
23+
run_test_suites_into_buffer,
24+
get_failed_unittests_from,
25+
vibrate_with_pyjnius,
26+
get_android_python_activity,
27+
set_device_orientation,
28+
)
29+
30+
31+
app = Flask(__name__)
32+
TESTS_TO_PERFORM = dict()
33+
NON_ANDROID_DEVICE_MSG = 'Not running from Android device'
34+
35+
36+
def get_html_for_tested_modules(tested_modules, failed_tests):
37+
modules_text = ''
38+
for n, module in enumerate(sorted(tested_modules)):
39+
print(module)
40+
base_text = '<label class="{color}">{module}</label>'
41+
if TESTS_TO_PERFORM[module] in failed_tests:
42+
color = 'text-red'
43+
else:
44+
color = 'text-green'
45+
if n != len(tested_modules) - 1:
46+
base_text += ', '
47+
48+
modules_text += base_text.format(color=color, module=module)
49+
50+
return Markup(modules_text)
51+
52+
53+
@app.route('/')
54+
def index():
55+
return render_template(
56+
'index.html',
57+
platform='Android' if RUNNING_ON_ANDROID else 'Desktop',
58+
)
59+
60+
61+
@app.route('/unittests')
62+
def unittests():
63+
import unittest
64+
print('Imported unittest')
65+
66+
print("loading tests...")
67+
suites = unittest.TestLoader().loadTestsFromNames(
68+
list(TESTS_TO_PERFORM.values()),
69+
)
70+
71+
print("running unittest...")
72+
terminal_output = run_test_suites_into_buffer(suites)
73+
74+
print("unittest result is:")
75+
unittest_error_text = terminal_output.split('\n')
76+
print(terminal_output)
77+
78+
# get a nice colored `html` output for our tested recipes
79+
failed_tests = get_failed_unittests_from(
80+
terminal_output, TESTS_TO_PERFORM.values(),
81+
)
82+
colored_tested_recipes = get_html_for_tested_modules(
83+
TESTS_TO_PERFORM.keys(), failed_tests,
84+
)
85+
86+
return render_template(
87+
'unittests.html',
88+
tested_recipes=colored_tested_recipes,
89+
unittests_output=unittest_error_text,
90+
platform='Android' if RUNNING_ON_ANDROID else 'Desktop',
91+
)
92+
93+
94+
@app.route('/page2')
95+
def page2():
96+
return render_template(
97+
'page2.html',
98+
platform='Android' if RUNNING_ON_ANDROID else 'Desktop',
99+
)
100+
101+
102+
@app.route('/loadUrl')
103+
def loadUrl():
104+
if not RUNNING_ON_ANDROID:
105+
print(NON_ANDROID_DEVICE_MSG, '...cancelled loadUrl.')
106+
return NON_ANDROID_DEVICE_MSG
107+
args = request.args
108+
if 'url' not in args:
109+
print('ERROR: asked to open an url but without url argument')
110+
print('asked to open url', args['url'])
111+
activity = get_android_python_activity()
112+
activity.loadUrl(args['url'])
113+
114+
115+
@app.route('/vibrate')
116+
def vibrate():
117+
if not RUNNING_ON_ANDROID:
118+
print(NON_ANDROID_DEVICE_MSG, '...cancelled vibrate.')
119+
return NON_ANDROID_DEVICE_MSG
120+
121+
args = request.args
122+
if 'time' not in args:
123+
print('ERROR: asked to vibrate but without time argument')
124+
print('asked to vibrate', args['time'])
125+
return vibrate_with_pyjnius(int(float(args['time']) * 1000))
126+
127+
128+
@app.route('/orientation')
129+
def orientation():
130+
if not RUNNING_ON_ANDROID:
131+
print(NON_ANDROID_DEVICE_MSG, '...cancelled orientation.')
132+
return NON_ANDROID_DEVICE_MSG
133+
args = request.args
134+
if 'dir' not in args:
135+
print('ERROR: asked to orient but no dir specified')
136+
return 'No direction specified '
137+
direction = args['dir']
138+
return set_device_orientation(direction)

0 commit comments

Comments
 (0)