diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index b47dac8810..611309c484 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -1,4 +1,4 @@ -name: Unit tests & Build Testapp +name: Unit tests & build apps on: ['push', 'pull_request'] @@ -42,25 +42,28 @@ jobs: make test build: - name: Build testapp + name: Unit test apk needs: [flake8] runs-on: ubuntu-latest strategy: + fail-fast: false matrix: - build-arch: ['arm64-v8a', 'armeabi-v7a'] + build-arch: ['arm64-v8a', 'armeabi-v7a', 'x86_64', 'x86'] steps: - name: Checkout python-for-android uses: actions/checkout@v2 - name: Pull docker image run: | make docker/pull - - name: Build apk for Python 3 ${{ matrix.build-arch }} + - name: Build apk python3 ${{ matrix.build-arch }} run: | mkdir -p apks - make docker/run/make/with-artifact/testapps/python3/${{ matrix.build-arch }} + make docker/run/make/with-artifact/testapps/python3-reqs-arch/$REQUIREMENTS/${{ matrix.build-arch }} + env: + REQUIREMENTS: sdl2,pyjnius,kivy,python3,openssl,requests,sqlite3,setuptools,numpy,kiwisolver,pandas,matplotlib,mplfinance - uses: actions/upload-artifact@v1 with: - name: bdisttest_python3_sqlite_openssl_googlendk__${{ matrix.build-arch }}-debug-1.1.apk + name: unit_tests_app__python3-${{ matrix.build-arch }}-debug-1.1.apk path: apks rebuild_updated_recipes: diff --git a/.travis.yml b/.travis.yml index 4e53f97194..b21c3c4aa8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -49,8 +49,10 @@ jobs: - &testapps name: Python 3 arm64-v8a (with numpy) stage: build testapps + env: + - REQUIREMENTS=sdl2,pyjnius,kivy,python3,openssl,requests,sqlite3,setuptools,numpy before_script: make docker/pull - script: make docker/run/make/testapps/python3/arm64-v8a + script: make docker/run/make/testapps/python3-reqs-arch/${REQUIREMENTS}/arm64-v8a - <<: *testapps name: Python 3 armeabi-v7a os: osx diff --git a/Dockerfile.py3 b/Dockerfile.py3 index cd6e9f8405..9183c8feaf 100644 --- a/Dockerfile.py3 +++ b/Dockerfile.py3 @@ -52,6 +52,7 @@ RUN dpkg --add-architecture i386 \ && ${RETRY} apt -y install -qq --no-install-recommends \ autoconf \ automake \ + autopoint \ build-essential \ ccache \ cmake \ diff --git a/Makefile b/Makefile index f1783c9820..8406183108 100644 --- a/Makefile +++ b/Makefile @@ -35,21 +35,26 @@ rebuild_updated_recipes: virtualenv ANDROID_SDK_HOME=$(ANDROID_SDK_HOME) ANDROID_NDK_HOME=$(ANDROID_NDK_HOME) \ $(PYTHON) ci/rebuild_updated_recipes.py -testapps/python2/armeabi-v7a: virtualenv - . $(ACTIVATE) && cd testapps/ && \ - python setup_testapp_python2_sqlite_openssl.py apk --sdk-dir $(ANDROID_SDK_HOME) --ndk-dir $(ANDROID_NDK_HOME) \ - --requirements sdl2,pyjnius,kivy,python2,openssl,requests,sqlite3,setuptools - -testapps/python3/arm64-v8a: virtualenv - . $(ACTIVATE) && cd testapps/ && \ - python setup_testapp_python3_sqlite_openssl.py apk --sdk-dir $(ANDROID_SDK_HOME) --ndk-dir $(ANDROID_NDK_HOME) \ - --requirements libffi,sdl2,pyjnius,kivy,python3,openssl,requests,sqlite3,setuptools,numpy \ - --arch=arm64-v8a - -testapps/python3/armeabi-v7a: virtualenv - . $(ACTIVATE) && cd testapps/ && \ - python setup_testapp_python3_sqlite_openssl.py apk --sdk-dir $(ANDROID_SDK_HOME) --ndk-dir $(ANDROID_NDK_HOME) \ - --arch=armeabi-v7a +testapps/python2/%: virtualenv + $(eval $@_APP_ARCH := $(shell basename $*)) + . $(ACTIVATE) && cd testapps/on_device_unit_tests/ && \ + python setup.py apk --sdk-dir $(ANDROID_SDK_HOME) --ndk-dir $(ANDROID_NDK_HOME) \ + --requirements sdl2,pyjnius,kivy,python2,openssl,requests,sqlite3,setuptools \ + --arch=$($@_APP_ARCH) + +testapps/python3-reqs-arch/%: virtualenv + $(eval $@_APP_ARCH := $(shell basename $*)) + $(eval $@_APP_REQS := $(shell basename $(dir $*))) + . $(ACTIVATE) && cd testapps/on_device_unit_tests/ && \ + python setup.py apk --sdk-dir $(ANDROID_SDK_HOME) --ndk-dir $(ANDROID_NDK_HOME) \ + --requirements $($@_APP_REQS) \ + --arch=$($@_APP_ARCH) + +testapps/python3/%: virtualenv + $(eval $@_APP_ARCH := $(shell basename $*)) + . $(ACTIVATE) && cd testapps/on_device_unit_tests/ && \ + python setup.py apk --sdk-dir $(ANDROID_SDK_HOME) --ndk-dir $(ANDROID_NDK_HOME) \ + --arch=$($@_APP_ARCH) clean: find . -type d -name "__pycache__" -exec rm -r {} + @@ -77,14 +82,9 @@ docker/run/make/%: docker/build docker run --rm --env-file=.env $(DOCKER_IMAGE) make $* docker/run/make/with-artifact/%: docker/build -ifeq (,$(findstring python3,$($*))) - $(eval $@_APP_NAME := bdisttest_python3_sqlite_openssl_googlendk) -else - $(eval $@_APP_NAME := bdisttest_python2_sqlite_openssl) -endif $(eval $@_APP_ARCH := $(shell basename $*)) docker run --name p4a-latest --env-file=.env $(DOCKER_IMAGE) make $* - docker cp p4a-latest:/home/user/app/testapps/$($@_APP_NAME)__$($@_APP_ARCH)-debug-1.1-.apk ./apks + docker cp p4a-latest:/home/user/app/testapps/on_device_unit_tests/bdist_unit_tests_app__$($@_APP_ARCH)-debug-1.1-.apk ./apks docker rm -fv p4a-latest docker/run/shell: docker/build diff --git a/ci/makefiles/osx.mk b/ci/makefiles/osx.mk index 0b274a6ab1..12f3888316 100644 --- a/ci/makefiles/osx.mk +++ b/ci/makefiles/osx.mk @@ -9,6 +9,10 @@ install_java: brew tap adoptopenjdk/openjdk brew cask install adoptopenjdk8 /usr/libexec/java_home -V + # install gettext (because it contains autopoint + # and we need that to build `liblzma` recipe) + brew install gettext + ln -s `brew ls gettext | grep bin/autopoint` /usr/local/bin upgrade_cython: pip3 install --upgrade Cython==0.28.6 diff --git a/ci/rebuild_updated_recipes.py b/ci/rebuild_updated_recipes.py index 54f62ac768..6f9dabecd0 100755 --- a/ci/rebuild_updated_recipes.py +++ b/ci/rebuild_updated_recipes.py @@ -55,18 +55,15 @@ def build(target_python, requirements): """ if not requirements: return - testapp = 'setup_testapp_python2.py' android_sdk_home = os.environ['ANDROID_SDK_HOME'] android_ndk_home = os.environ['ANDROID_NDK_HOME'] - if target_python == TargetPython.python3: - testapp = 'setup_testapp_python3_sqlite_openssl.py' requirements.add(target_python.name) requirements = ','.join(requirements) logger.info('requirements: {}'.format(requirements)) - with current_directory('testapps/'): + with current_directory('testapps/on_device_unit_tests/'): # iterates to stream the output for line in sh.python( - testapp, 'apk', '--sdk-dir', android_sdk_home, + 'setup.py', 'apk', '--sdk-dir', android_sdk_home, '--ndk-dir', android_ndk_home, '--requirements', requirements, _err_to_out=True, _iter=True): print(line) diff --git a/doc/source/testing_pull_requests.rst b/doc/source/testing_pull_requests.rst index 603828db77..a133cb15be 100644 --- a/doc/source/testing_pull_requests.rst +++ b/doc/source/testing_pull_requests.rst @@ -103,30 +103,31 @@ Using python-for-android commands directly from the pull request files ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - Enter inside the directory of the cloned repository in the above - step and run p4a command with proper args, eg:: + step and run p4a command with proper args, eg (to test an modified + `pycryptodome` recipe):: .. codeblock:: bash cd p4a-feature-fix-numpy PYTHONPATH=. python3 -m pythonforandroid.toolchain apk \ - --private=testapps/testapp_sqlite_openssl \ - --dist-name=dist_test_app_python3_libs \ + --private=testapps/on_device_unit_tests/test_app \ + --dist-name=dist_unit_tests_app_pycryptodome \ --package=org.kivy \ - --name=test_app_python3_sqlite_openssl \ + --name=unit_tests_app_pycryptodome \ --version=0.1 \ - --requirements=requests,peewee,sdl2,pyjnius,kivy,python3 \ + --requirements=sdl2,pyjnius,kivy,python3,pycryptodome \ --ndk-dir=/media/DEVEL/Android/android-ndk-r20 \ --sdk-dir=/media/DEVEL/Android/android-sdk-linux \ --android-api=27 \ --arch=arm64-v8a \ - --permission=INTERNET \ + --permission=VIBRATE \ --debug Things that you should know: - - The example above will build an testapp we will make use of the files of - the testapp named `testapp_sqlite_openssl.py` but we don't use the setup + - The example above will build an test app we will make use of the files of + the `on device unit tests` test app but we don't use the setup file to build it so we must tell python-for-android what we want via arguments - be sure to at least edit the following arguments when running the above @@ -159,19 +160,19 @@ Installing python-for-android using the github's branch of the pull request cd p4a-feature-fix-numpy pip3 install . --upgrade --user -- Now, go inside the `testapps` directory (we assume that you still are inside - the cloned repository):: +- Now, go inside the `testapps/on_device_unit_tests` directory (we assume that + you still are inside the cloned repository):: .. codeblock:: bash - cd testapps + cd testapps/on_device_unit_tests - Run the build of the apk via the freshly installed copy of python-for-android by running a similar command than below:: .. code-block:: bash - python3 setup_testapp_python3_sqlite_openssl.py apk \ + python3 setup.py apk \ --ndk-dir=/media/DEVEL/Android/android-ndk-r20 \ --sdk-dir=/media/DEVEL/Android/android-sdk-linux \ --android-api=27 \ @@ -182,8 +183,7 @@ Installing python-for-android using the github's branch of the pull request Things that you should know: - In the example above, we override some variables that are set in - `setup_testapp_python3_sqlite_openssl.py`, you could also override them - by editing this file + `setup.py`, you could also override them by editing this file - be sure to at least edit the following arguments when running the above command, since the default set in there it's unlikely that match your installation: diff --git a/pythonforandroid/python.py b/pythonforandroid/python.py index d2235b5679..9c8b2439a2 100755 --- a/pythonforandroid/python.py +++ b/pythonforandroid/python.py @@ -169,6 +169,14 @@ def add_flags(include_flags, link_dirs, link_libs): add_flags(recipe.include_flags(arch), recipe.link_dirs_flags(arch), recipe.link_libs_flags()) + for library_name in {'libbz2', 'liblzma'}: + if library_name in self.ctx.recipe_build_order: + info(f'Activating flags for {library_name}') + recipe = Recipe.get_recipe(library_name, self.ctx) + add_flags(recipe.get_library_includes(arch), + recipe.get_library_ldflags(arch), + recipe.get_library_libs_flag()) + # python build system contains hardcoded zlib version which prevents # the build of zlib module, here we search for android's zlib version # and sets the right flags, so python can be build with android's zlib diff --git a/pythonforandroid/recipes/libbz2/__init__.py b/pythonforandroid/recipes/libbz2/__init__.py new file mode 100644 index 0000000000..8bd806cb57 --- /dev/null +++ b/pythonforandroid/recipes/libbz2/__init__.py @@ -0,0 +1,54 @@ +from multiprocessing import cpu_count + +from pythonforandroid.archs import Arch +from pythonforandroid.recipe import Recipe +from pythonforandroid.logger import shprint +from pythonforandroid.util import current_directory +import sh + + +class LibBz2Recipe(Recipe): + + # there is not an official release yet... + # so pinned to last commit at the time of writing + version = "1.0.8" + url = "https://sourceware.org/pub/bzip2/bzip2-{version}.tar.gz" + built_libraries = {"libbz2.so": ""} + patches = ["lib_android.patch"] + + def build_arch(self, arch: Arch) -> None: + env = self.get_recipe_env(arch) + with current_directory(self.get_build_dir(arch.arch)): + shprint( + sh.make, + "-j", + str(cpu_count()), + f'CC={env["CC"]}', + f'AR={env["AR"]}', + f'RANLIB={env["RANLIB"]}', + "-f", + "Makefile-libbz2_so", + _env=env, + ) + + def get_library_includes(self, arch: Arch) -> str: + """Returns a string with the appropriate `-I` to link + with the bz2 lib. This string is usually added to the environment + variable `CPPFLAGS`""" + return " -I" + self.get_build_dir(arch.arch) + + def get_library_ldflags(self, arch: Arch) -> str: + """Returns a string with the appropriate `-L` to link + with the bz2 lib. This string is usually added to the environment + variable `LDFLAGS`""" + return " -L" + self.get_build_dir(arch.arch) + + @staticmethod + def get_library_libs_flag() -> str: + """Returns a string with the appropriate `-l` flags to link with + the bz2 lib. This string is usually added to the environment + variable `LIBS`""" + return " -lbz2" + + +recipe = LibBz2Recipe() diff --git a/pythonforandroid/recipes/libbz2/lib_android.patch b/pythonforandroid/recipes/libbz2/lib_android.patch new file mode 100644 index 0000000000..b208896ba5 --- /dev/null +++ b/pythonforandroid/recipes/libbz2/lib_android.patch @@ -0,0 +1,29 @@ +Set default compiler to `clang` and disable versioned shared library +--- bzip2-1.0.8/Makefile-libbz2_so.orig 2019-07-13 19:50:05.000000000 +0200 ++++ bzip2-1.0.8/Makefile-libbz2_so 2020-03-13 20:10:32.336990786 +0100 +@@ -22,7 +22,7 @@ + + + SHELL=/bin/sh +-CC=gcc ++CC=clang + BIGFILES=-D_FILE_OFFSET_BITS=64 + CFLAGS=-fpic -fPIC -Wall -Winline -O2 -g $(BIGFILES) + +@@ -35,13 +35,11 @@ OBJS= blocksort.o \ + bzlib.o + + all: $(OBJS) +- $(CC) -shared -Wl,-soname -Wl,libbz2.so.1.0 -o libbz2.so.1.0.8 $(OBJS) +- $(CC) $(CFLAGS) -o bzip2-shared bzip2.c libbz2.so.1.0.8 +- rm -f libbz2.so.1.0 +- ln -s libbz2.so.1.0.8 libbz2.so.1.0 ++ $(CC) -shared -Wl,-soname=libbz2.so -o libbz2.so $(OBJS) ++ $(CC) $(CFLAGS) -o bzip2-shared bzip2.c libbz2.so + + clean: +- rm -f $(OBJS) bzip2.o libbz2.so.1.0.8 libbz2.so.1.0 bzip2-shared ++ rm -f $(OBJS) bzip2.o libbz2.so bzip2-shared + + blocksort.o: blocksort.c + $(CC) $(CFLAGS) -c blocksort.c diff --git a/pythonforandroid/recipes/liblzma/__init__.py b/pythonforandroid/recipes/liblzma/__init__.py new file mode 100644 index 0000000000..a91e2cb0f8 --- /dev/null +++ b/pythonforandroid/recipes/liblzma/__init__.py @@ -0,0 +1,71 @@ +from os.path import exists, join +from multiprocessing import cpu_count + +from pythonforandroid.archs import Arch +from pythonforandroid.recipe import Recipe +from pythonforandroid.logger import shprint +from pythonforandroid.util import current_directory, ensure_dir +import sh + + +class LibLzmaRecipe(Recipe): + + version = '5.2.4' + url = 'https://tukaani.org/xz/xz-{version}.tar.gz' + built_libraries = {'liblzma.so': 'install/lib'} + + def build_arch(self, arch: Arch) -> None: + env = self.get_recipe_env(arch) + install_dir = join(self.get_build_dir(arch.arch), 'install') + with current_directory(self.get_build_dir(arch.arch)): + if not exists('configure'): + shprint(sh.Command('./autogen.sh'), _env=env) + shprint(sh.Command('autoreconf'), '-vif', _env=env) + shprint(sh.Command('./configure'), + '--host=' + arch.command_prefix, + '--prefix=' + install_dir, + '--disable-builddir', + '--disable-static', + '--enable-shared', + + '--disable-xz', + '--disable-xzdec', + '--disable-lzmadec', + '--disable-lzmainfo', + '--disable-scripts', + '--disable-doc', + + _env=env) + shprint( + sh.make, '-j', str(cpu_count()), + _env=env + ) + + ensure_dir('install') + shprint(sh.make, 'install', _env=env) + + def get_library_includes(self, arch: Arch) -> str: + """Returns a string with the appropriate `-I` to link + with the lzma lib. This string is usually added to the environment + variable `CPPFLAGS`""" + return " -I" + join( + self.get_build_dir(arch.arch), 'install', 'include', + ) + + def get_library_ldflags(self, arch: Arch) -> str: + """Returns a string with the appropriate `-L` to link + with the lzma lib. This string is usually added to the environment + variable `LDFLAGS`""" + return " -L" + join( + self.get_build_dir(arch.arch), self.built_libraries['liblzma.so'], + ) + + @staticmethod + def get_library_libs_flag() -> str: + """Returns a string with the appropriate `-l` flags to link with + the lzma lib. This string is usually added to the environment + variable `LIBS`""" + return " -llzma" + + +recipe = LibLzmaRecipe() diff --git a/pythonforandroid/recipes/mplfinance/__init__.py b/pythonforandroid/recipes/mplfinance/__init__.py new file mode 100644 index 0000000000..539c1f0f84 --- /dev/null +++ b/pythonforandroid/recipes/mplfinance/__init__.py @@ -0,0 +1,18 @@ + +from pythonforandroid.recipe import PythonRecipe + + +class MplfinanceRecipe(PythonRecipe): + + # there is not an official release yet... + # so pinned to last commit at the time of writing + version = 'cde5008' + url = 'https://github.com/matplotlib/mplfinance/archive/{version}.zip' + + depends = ['setuptools', 'matplotlib', 'pandas'] + conflicts = ['python2'] + + call_hostpython_via_targetpython = False + + +recipe = MplfinanceRecipe() diff --git a/pythonforandroid/recipes/pandas/__init__.py b/pythonforandroid/recipes/pandas/__init__.py new file mode 100644 index 0000000000..34bb30cbe3 --- /dev/null +++ b/pythonforandroid/recipes/pandas/__init__.py @@ -0,0 +1,35 @@ + +from os.path import join +from pythonforandroid.recipe import CppCompiledComponentsPythonRecipe + + +class PandasRecipe(CppCompiledComponentsPythonRecipe): + version = '1.0.2' + url = 'https://github.com/pandas-dev/pandas/releases/download/v{version}/pandas-{version}.tar.gz' # noqa + + depends = ['cython', 'numpy', 'pytz', 'libbz2', 'liblzma'] + conflicts = ['python2'] + + python_depends = ['python-dateutil'] + patches = ['fix_numpy_includes.patch'] + + call_hostpython_via_targetpython = False + + def get_recipe_env(self, arch): + env = super(PandasRecipe, self).get_recipe_env(arch) + # we need the includes from our installed numpy at site packages + # because we need some includes generated at numpy's compile time + env['NUMPY_INCLUDES'] = join( + self.ctx.get_python_install_dir(), "numpy/core/include", + ) + + # this flag below is to fix a runtime error: + # ImportError: dlopen failed: cannot locate symbol + # "_ZTVSt12length_error" referenced by + # "/data/data/org.test.matplotlib_testapp/files/app/_python_bundle + # /site-packages/pandas/_libs/window/aggregations.so"... + env['LDFLAGS'] += ' -landroid' + return env + + +recipe = PandasRecipe() diff --git a/pythonforandroid/recipes/pandas/fix_numpy_includes.patch b/pythonforandroid/recipes/pandas/fix_numpy_includes.patch new file mode 100644 index 0000000000..ef1643b9b1 --- /dev/null +++ b/pythonforandroid/recipes/pandas/fix_numpy_includes.patch @@ -0,0 +1,31 @@ +--- pandas-1.0.1/setup.py.orig 2020-02-05 17:15:24.000000000 +0100 ++++ pandas-1.0.1/setup.py 2020-03-15 13:47:57.612237225 +0100 +@@ -37,11 +37,12 @@ min_cython_ver = "0.29.13" # note: sync + + setuptools_kwargs = { + "install_requires": [ +- "python-dateutil >= 2.6.1", +- "pytz >= 2017.2", +- f"numpy >= {min_numpy_ver}", ++ # dependencies managed via p4a's recipe ++ # "python-dateutil >= 2.6.1", ++ # "pytz >= 2017.2", ++ # f"numpy >= {min_numpy_ver}", + ], +- "setup_requires": [f"numpy >= {min_numpy_ver}"], ++ "setup_requires": [], + "zip_safe": False, + } + +@@ -514,7 +515,10 @@ def maybe_cythonize(extensions, *args, * + ) + raise RuntimeError("Cannot cythonize without Cython installed.") + +- numpy_incl = pkg_resources.resource_filename("numpy", "core/include") ++ if 'NUMPY_INCLUDES' in os.environ: ++ numpy_incl = os.environ['NUMPY_INCLUDES'] ++ else: ++ numpy_incl = pkg_resources.resource_filename("numpy", "core/include") + # TODO: Is this really necessary here? + for ext in extensions: + if hasattr(ext, "include_dirs") and numpy_incl not in ext.include_dirs: diff --git a/pythonforandroid/recipes/python3/__init__.py b/pythonforandroid/recipes/python3/__init__.py index 09b5c85cc9..5a319ea670 100644 --- a/pythonforandroid/recipes/python3/__init__.py +++ b/pythonforandroid/recipes/python3/__init__.py @@ -39,6 +39,10 @@ class Python3Recipe(GuestPythonRecipe): ] depends = ['hostpython3', 'sqlite3', 'openssl', 'libffi'] + # those optional depends allow us to build python compression modules: + # - _bz2.so + # - _lzma.so + opt_depends = ['libbz2', 'liblzma'] conflicts = ['python2'] configure_args = ( diff --git a/testapps/on_device_unit_tests/README.rst b/testapps/on_device_unit_tests/README.rst index 1415fbb6a4..6c0eb9626e 100644 --- a/testapps/on_device_unit_tests/README.rst +++ b/testapps/on_device_unit_tests/README.rst @@ -4,18 +4,38 @@ On device unit tests This test app runs a set of unit tests, to help confirm that the python-for-android build is actually working properly. +Also it's dynamic, because it will run one app or another depending on the +supplied recipes at build time. + +It currently supports three app `modes`: + - `kivy app` (with sdl2 bootstrap): if kivy in recipes + - `flask app` (with webview bootstrap): if flask in recipes + - `no gui`: if neither of above cases is taken + The main tests are for the recipes built in the apk. Each module (or other tool) is at least imported and subject to some basic check. -This app is experimental, it doesn't yet support things like testing -only the requirements you ask for (so if you build with requirements -other than those specified, the tests may fail). It also has no gui -yet, the results must be checked via logcat. +This test app can be build via `setup.py` or via buildozer. In both +cases it will build a basic kivy app with a set of tests defined via the +`requirements` keyword (specified at build time). + +In case that you build the `test app with no-gui`, the unittests results must +be checked via command `adb logcat` or some logging apk (you may need root +permissions in your device to use such app). + +Building the app with python-for-android +======================================== + +You can use the provided file `setup.py`. Check our `Makefile +`__ to guess +how to build the test app, or also you can look at `testing pull requests documentation +`__, +which describes some of the methods that you can use to build the test app. -Building the app -================ +Building the app with buildozer +=============================== -This app should be built using buildozer, which it also serves as a +This app can be built using buildozer, which it also serves as a test for:: $ buildozer android debug diff --git a/testapps/on_device_unit_tests/buildozer.spec b/testapps/on_device_unit_tests/buildozer.spec index a2b55302c8..b372d5faa5 100644 --- a/testapps/on_device_unit_tests/buildozer.spec +++ b/testapps/on_device_unit_tests/buildozer.spec @@ -13,7 +13,7 @@ package.domain = org.kivy source.dir = test_app # (list) Source files to include (let empty to include all the files) -source.include_exts = py,png,jpg,kv,atlas +source.include_exts = py,png,jpg,kv,atlas,html,css,otf,txt # (list) List of inclusions using pattern matching #source.include_patterns = assets/*,images/*.png @@ -36,7 +36,7 @@ version = 0.1 # (list) Application requirements # comma separated e.g. requirements = sqlite3,kivy -requirements = python3,kivy,openssl,numpy,sqlite3 +requirements = python3,kivy,libffi,openssl,numpy,sqlite3 # (str) Custom source folders for requirements # Sets custom source for any requirements with recipes @@ -52,7 +52,7 @@ requirements = python3,kivy,openssl,numpy,sqlite3 #icon.filename = %(source.dir)s/data/icon.png # (str) Supported orientation (one of landscape, sensorLandscape, portrait or all) -orientation = portrait +orientation = all # (list) List of service to declare #services = NAME:ENTRYPOINT_TO_PY,NAME2:ENTRYPOINT2_TO_PY diff --git a/testapps/on_device_unit_tests/setup.py b/testapps/on_device_unit_tests/setup.py new file mode 100644 index 0000000000..d1b2abbe67 --- /dev/null +++ b/testapps/on_device_unit_tests/setup.py @@ -0,0 +1,92 @@ +""" +This is the `setup.py` file for the `on device unit test app`. + +In this module we can control how will be built our test app. Depending on +our requirements we can build an kivy, flask or a non-gui app. We default to an +kivy app, since the python-for-android project its a sister project of kivy. + +The parameter `requirements` is crucial to determine the unit tests we will +perform with our app, so we must explicitly name the recipe we want to test +and, of course, we should have the proper test for the given recipe at +`tests.test_requirements.py` or nothing will be tested. We control our default +app requirements via the dictionary `options`. Here you have some examples +to build the supported app modes:: + + - kivy *basic*: `sqlite3,libffi,openssl,pyjnius,kivy,python3,requests` + - kivy *images/graphs*: `kivy,python3,numpy,matplotlib,Pillow` + - kivy *encryption*: `kivy,python3,cryptography,pycryptodome,scrypt, + m2crypto,pysha3` + - flask (with webview bootstrap): `sqlite3,libffi,openssl,pyjnius,flask, + python3,genericndkbuild` + + +.. note:: just noting that, for the `kivy basic` app, we add the requirements: + `sqlite3,libffi,openssl` so this way we will trigger the unit tests + that we have for such recipes. + +.. tip:: to force `python-for-android` generate an `flask` app without using + the kwarg `bootstrap`, we add the recipe `genericndkbuild`, which will + trigger the `webview bootstrap` at build time. +""" + +import os +import sys +from distutils.core import setup +from setuptools import find_packages + +# define a basic test app, which can be override passing the proper args to cli +options = { + 'apk': + { + 'requirements': + 'sqlite3,libffi,openssl,pyjnius,kivy,python3,requests', + 'android-api': 27, + 'ndk-api': 21, + 'dist-name': 'bdist_unit_tests_app', + 'arch': 'armeabi-v7a', + 'permissions': ['INTERNET', 'VIBRATE'], + 'orientation': 'sensor', + 'service': 'P4a_test_service:app_service.py', + } +} + +# check if we overwrote the default test_app requirements via `cli` +requirements = options['apk']['requirements'].rsplit(',') +for n, arg in enumerate(sys.argv): + if arg == '--requirements': + print('found requirements') + requirements = sys.argv[n + 1].rsplit(',') + break + +# remove `orientation` in case that we don't detect a kivy or flask app, +# since the `service_only` bootstrap does not support such argument +if not ({'kivy', 'flask'} & set(requirements)): + options['apk'].pop('orientation') + +# write a file to let the test_app know which requirements we want to test +# Note: later, when running the app, we will guess if we have the right test. +app_requirements_txt = os.path.join( + os.path.split(__file__)[0], + 'test_app', + 'app_requirements.txt', +) +with open(app_requirements_txt, 'w') as requirements_file: + for req in requirements: + requirements_file.write(f'{req.split("==")[0]}\n') + +# run the install +setup( + name='unit_tests_app', + version='1.1', + description='p4a on device unit test app', + author='Alexander Taylor, Pol Canelles', + author_email='alexanderjohntaylor@gmail.com, canellestudi@gmail.com', + packages=find_packages(), + options=options, + package_data={ + 'test_app': ['*.py', '*.kv', '*.txt'], + 'test_app/static': ['*.png', '*.css', '*.otf'], + 'test_app/templates': ['*.html'], + 'test_app/tests': ['*.py', '*.csv'], + } +) diff --git a/testapps/on_device_unit_tests/test_app/app_flask.py b/testapps/on_device_unit_tests/test_app/app_flask.py new file mode 100644 index 0000000000..ee23532035 --- /dev/null +++ b/testapps/on_device_unit_tests/test_app/app_flask.py @@ -0,0 +1,138 @@ +print('main.py was successfully called') +print('this is the new main.py') + +import sys +print('python version is: ' + sys.version) +print('python path is', sys.path) + +import os +print('imported os') +print('contents of this dir', os.listdir('./')) + +from flask import ( + Flask, + render_template, + request, + Markup +) + +print('imported flask etc') + +from constants import RUNNING_ON_ANDROID +from tools import ( + run_test_suites_into_buffer, + get_failed_unittests_from, + vibrate_with_pyjnius, + get_android_python_activity, + set_device_orientation, +) + + +app = Flask(__name__) +TESTS_TO_PERFORM = dict() +NON_ANDROID_DEVICE_MSG = 'Not running from Android device' + + +def get_html_for_tested_modules(tested_modules, failed_tests): + modules_text = '' + for n, module in enumerate(sorted(tested_modules)): + print(module) + base_text = '' + if TESTS_TO_PERFORM[module] in failed_tests: + color = 'text-red' + else: + color = 'text-green' + if n != len(tested_modules) - 1: + base_text += ', ' + + modules_text += base_text.format(color=color, module=module) + + return Markup(modules_text) + + +@app.route('/') +def index(): + return render_template( + 'index.html', + platform='Android' if RUNNING_ON_ANDROID else 'Desktop', + ) + + +@app.route('/unittests') +def unittests(): + import unittest + print('Imported unittest') + + print("loading tests...") + suites = unittest.TestLoader().loadTestsFromNames( + list(TESTS_TO_PERFORM.values()), + ) + + print("running unittest...") + terminal_output = run_test_suites_into_buffer(suites) + + print("unittest result is:") + unittest_error_text = terminal_output.split('\n') + print(terminal_output) + + # get a nice colored `html` output for our tested recipes + failed_tests = get_failed_unittests_from( + terminal_output, TESTS_TO_PERFORM.values(), + ) + colored_tested_recipes = get_html_for_tested_modules( + TESTS_TO_PERFORM.keys(), failed_tests, + ) + + return render_template( + 'unittests.html', + tested_recipes=colored_tested_recipes, + unittests_output=unittest_error_text, + platform='Android' if RUNNING_ON_ANDROID else 'Desktop', + ) + + +@app.route('/page2') +def page2(): + return render_template( + 'page2.html', + platform='Android' if RUNNING_ON_ANDROID else 'Desktop', + ) + + +@app.route('/loadUrl') +def loadUrl(): + if not RUNNING_ON_ANDROID: + print(NON_ANDROID_DEVICE_MSG, '...cancelled loadUrl.') + return NON_ANDROID_DEVICE_MSG + args = request.args + if 'url' not in args: + print('ERROR: asked to open an url but without url argument') + print('asked to open url', args['url']) + activity = get_android_python_activity() + activity.loadUrl(args['url']) + + +@app.route('/vibrate') +def vibrate(): + if not RUNNING_ON_ANDROID: + print(NON_ANDROID_DEVICE_MSG, '...cancelled vibrate.') + return NON_ANDROID_DEVICE_MSG + + args = request.args + if 'time' not in args: + print('ERROR: asked to vibrate but without time argument') + print('asked to vibrate', args['time']) + return vibrate_with_pyjnius(int(float(args['time']) * 1000)) + + +@app.route('/orientation') +def orientation(): + if not RUNNING_ON_ANDROID: + print(NON_ANDROID_DEVICE_MSG, '...cancelled orientation.') + return NON_ANDROID_DEVICE_MSG + args = request.args + if 'dir' not in args: + print('ERROR: asked to orient but no dir specified') + return 'No direction specified ' + direction = args['dir'] + return set_device_orientation(direction) diff --git a/testapps/on_device_unit_tests/test_app/app_kivy.py b/testapps/on_device_unit_tests/test_app/app_kivy.py new file mode 100644 index 0000000000..8095998819 --- /dev/null +++ b/testapps/on_device_unit_tests/test_app/app_kivy.py @@ -0,0 +1,163 @@ +# -*- coding: utf-8 -*- + +import subprocess + +from os.path import split + +from kivy.app import App +from kivy.clock import Clock +from kivy.properties import ( + BooleanProperty, + DictProperty, + ListProperty, + StringProperty, +) +from kivy.lang import Builder + +from constants import RUNNING_ON_ANDROID +from tools import ( + get_android_python_activity, + get_failed_unittests_from, + get_images_with_extension, + load_kv_from, + raise_error, + run_test_suites_into_buffer, + vibrate_with_pyjnius, +) +from widgets import TestImage + +# define our app's screen manager and load the screen templates +screen_manager_app = ''' +ScreenManager: + ScreenUnittests: + ScreenKeyboard: + ScreenOrientation: + ScreenService: +''' +load_kv_from('screen_unittests.kv') +load_kv_from('screen_keyboard.kv') +load_kv_from('screen_orientation.kv') +load_kv_from('screen_service.kv') + + +class TestKivyApp(App): + + tests_to_perform = DictProperty() + unittest_error_text = StringProperty('Running unittests...') + test_packages = StringProperty('Unittest recipes:') + generated_images = ListProperty() + service_running = BooleanProperty(False) + + def build(self): + self.reset_unittests_results() + self.sm = Builder.load_string(screen_manager_app) + return self.sm + + def reset_unittests_results(self, refresh_ui=False): + for img in get_images_with_extension(): + subprocess.call(["rm", "-r", img]) + print('removed image: ', img) + if refresh_ui: + self.set_color_for_tested_modules(restart=True) + self.unittest_error_text = '' + screen_unittests = self.sm.get_screen('unittests') + images_box = screen_unittests.ids.test_images_box + images_box.clear_widgets() + self.generated_images = [] + + def on_tests_to_perform(self, *args): + """ + Check `test_to_perform` so we can build some special tests in our ui. + Also will schedule the run of our tests. + """ + print('on_tests_to_perform: ', self.tests_to_perform.keys()) + self.set_color_for_tested_modules(restart=True) + Clock.schedule_once(self.run_unittests, 3) + + def run_unittests(self, *args): + import unittest + print('Imported unittest') + + print("loading tests...") + suites = unittest.TestLoader().loadTestsFromNames( + list(self.tests_to_perform.values()), + ) + self.test_packages = ', '.join(self.tests_to_perform.keys()) + + print("running unittest...") + self.unittest_error_text = run_test_suites_into_buffer(suites) + + print("unittest result is:") + print(self.unittest_error_text) + print('Ran tests') + + self.set_color_for_tested_modules() + + # check generated images by unittests + self.generated_images = get_images_with_extension() + + def set_color_for_tested_modules(self, restart=False): + tests_made = sorted(list(self.tests_to_perform.keys())) + failed_tests = get_failed_unittests_from( + self.unittest_error_text, + self.tests_to_perform.values(), + ) + + modules_text = 'Unittest recipes: ' + for n, module in enumerate(tests_made): + base_text = '[color={color}]{module}[/color]' + if restart: + color = '#838383' # grey + elif self.tests_to_perform[module] in failed_tests: + color = '#ff0000' # red + else: + color = '#5d8000' # green + if n != len(tests_made) - 1: + base_text += ', ' + + modules_text += base_text.format(color=color, module=module) + + self.test_packages = modules_text + + def on_generated_images(self, *args): + screen_unittests = self.sm.get_screen('unittests') + images_box = screen_unittests.ids.test_images_box + for i in self.generated_images: + img = TestImage( + text='Generated image by unittests: {}'.format(split(i)[1]), + source=i, + ) + images_box.add_widget(img) + + def test_vibration_with_pyjnius(self, *args): + vibrate_with_pyjnius() + + @property + def service_time(self): + from jnius import autoclass + + return autoclass('org.test.unit_tests_app.ServiceP4a_test_service') + + def on_service_running(self, *args): + if RUNNING_ON_ANDROID: + if self.service_running: + print('Starting service') + self.start_service() + else: + print('Stopping service') + self.stop_service() + else: + raise_error('Service test not supported on desktop') + + def start_service(self): + activity = get_android_python_activity() + service = self.service_time + try: + service.start(activity, 'Some argument') + except Exception as err: + raise_error(str(err)) + + def stop_service(self): + service = self.service_time + activity = get_android_python_activity() + service.stop(activity) diff --git a/testapps/on_device_unit_tests/test_app/app_service.py b/testapps/on_device_unit_tests/test_app/app_service.py new file mode 100644 index 0000000000..953d865632 --- /dev/null +++ b/testapps/on_device_unit_tests/test_app/app_service.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import datetime +import threading +import time + +from os import environ + +argument = environ.get('PYTHON_SERVICE_ARGUMENT', '') +print( + 'app_service.py was successfully called with argument: "{}"'.format( + argument, + ), +) + +next_call = time.time() +next_call_in = 5 # seconds + + +def service_timer(): + global next_call + print('P4a test service: {}'.format(datetime.datetime.now())) + + next_call += next_call_in + threading.Timer(next_call - time.time(), service_timer).start() + + +print('Starting the test service timer...') +service_timer() diff --git a/testapps/on_device_unit_tests/test_app/constants.py b/testapps/on_device_unit_tests/test_app/constants.py new file mode 100644 index 0000000000..55b61d48b5 --- /dev/null +++ b/testapps/on_device_unit_tests/test_app/constants.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- + +from os import environ + +RUNNING_ON_ANDROID = "ANDROID_APP_PATH" in environ + +FONT_SIZE_TITLE = 32 if RUNNING_ON_ANDROID else 60 +FONT_SIZE_SUBTITLE = 16 if RUNNING_ON_ANDROID else 32 +FONT_SIZE_TEXT = 8 if RUNNING_ON_ANDROID else 16 diff --git a/testapps/on_device_unit_tests/test_app/main.py b/testapps/on_device_unit_tests/test_app/main.py index e0cd9c527d..349890d4ce 100644 --- a/testapps/on_device_unit_tests/test_app/main.py +++ b/testapps/on_device_unit_tests/test_app/main.py @@ -1,45 +1,96 @@ -import sys -if sys.version_info.major < 3: - print(('Running under Python {} but these tests ' - 'require Python 3+').format(sys.version_info.major)) - -import unittest -import importlib +#!/usr/bin/env python +# -*- coding: utf-8 -*- -print('Imported unittest') +""" +On device unit test app +======================= +This is a dynamic test app, which means that depending on the requirements +supplied at build time, will perform some tests or others. Also, this app will +have an ui, or not, depending on requirements as well. -class PythonTestMixIn(object): +For now, we contemplate three possibilities: + - A kivy unittest app (sdl2 bootstrap) + - A unittest app (webview bootstrap) + - A non-gui unittests app - module_import = None +If you install/build this app via the `setup.py` file, a file named +`app_requirements.txt` will be generated which will contain the requirements +that we passed to the `setup.py` via arguments, which will determine +the unittests that this app will run. - def test_import_module(self): - """Test importing the specified Python module name. This import test - is common to all Python modules, it does not test any further - functionality. - """ - self.assertIsNotNone( - self.module_import, - 'module_import is not set (was default None)') +.. note:: This app is made to be working on desktop and on an android device. + Be aware that some of the functionality of this app will only work on + an android device. - importlib.import_module(self.module_import) +.. tip:: you can write more unit tests at `tests/test_requirements.py` and test + these on desktop just by editing the file `app_requirements.txt`, + which should be located at the same location than this file. This + `app_requirements.txt` file, it's autogenerated when the + `setup.py` is ran, so in certain circumstances, you may need + to create it. Also be aware that each `python-for-android` recipe + that you want to test should be in a new line, taking into account the + case of the recipe. - def test_run_module(self): - """Import the specified module and do something with it as a minimal - check that it actually works. +.. warning:: If you use buildozer you only will get the basic `kivy unittest + app`, with a basic set of tests: sqlite3, libffi, openssl and + pyjnius. +""" - This test fails by default, it must be overridden by every - child test class. - """ +import sys +import unittest - self.fail('This test must be overridden by {}'.format(self)) +from os import curdir +from os.path import isfile, realpath -print('Defined test case') +print('Imported unittest') -import sys sys.path.append('./') -from tests import test_requirements -suite = unittest.TestLoader().loadTestsFromModule(test_requirements) -unittest.TextTestRunner().run(suite) -print('Ran tests') +# read `app_requirements.txt` and find out which tests to perform +tests_to_perform = {} +requirements = None +if isfile('app_requirements.txt'): + with open('app_requirements.txt', 'r') as requirements_file: + requirements = set(requirements_file.read().splitlines()) +if not requirements: + # we will test a basic set of recipes + requirements = {'sqlite3', 'libffi', 'openssl', 'pyjnius'} +print('App requirements are: ', requirements) + +for recipe in requirements: + test_name = 'tests.test_requirements.{recipe}TestCase'.format( + recipe=recipe.capitalize() + ) + try: + exist_test = unittest.TestLoader().loadTestsFromName(test_name) + except AttributeError: + # python2 case + pass + else: + if '_exception' not in exist_test._tests[0].__dict__: + print('Adding Testcase: ', test_name) + tests_to_perform[recipe] = test_name +print('Tests to perform are: ', tests_to_perform) + +# Find out which app we want to run +if 'kivy' in requirements: + from app_kivy import TestKivyApp + + test_app = TestKivyApp() + test_app.tests_to_perform = tests_to_perform + test_app.run() +elif 'flask' in requirements: + import app_flask + app_flask.TESTS_TO_PERFORM = tests_to_perform + + print('Current directory is ', realpath(curdir)) + if realpath(curdir).startswith('/data'): + app_flask.app.run(debug=False) + else: + app_flask.app.run(debug=True) +else: + # we don't have kivy or flask in our + # requirements, so we run unittests in terminal + suite = unittest.TestLoader().loadTestsFromNames(list(tests_to_perform)) + unittest.TextTestRunner().run(suite) diff --git a/testapps/on_device_unit_tests/test_app/screen_keyboard.kv b/testapps/on_device_unit_tests/test_app/screen_keyboard.kv new file mode 100644 index 0000000000..d0b08bbd67 --- /dev/null +++ b/testapps/on_device_unit_tests/test_app/screen_keyboard.kv @@ -0,0 +1,109 @@ +#:import Window kivy.core.window.Window + +#:import FONT_SIZE_SUBTITLE constants.FONT_SIZE_SUBTITLE +#:import FONT_SIZE_TEXT constants.FONT_SIZE_TEXT +#:import FONT_SIZE_TITLE constants.FONT_SIZE_TITLE +#:import Spacer20 widgets.Spacer20 + +: + name: 'keyboard' + BoxLayout: + orientation: 'vertical' + Button: + text: 'Back to unittests' + font_size: sp(FONT_SIZE_SUBTITLE) + size_hint_y: None + height: dp(60) + on_press: root.parent.current = 'unittests' + Image: + keep_ratio: False + allow_stretch: True + source: 'static/coloursinv.png' + size_hint_y: None + height: dp(100) + Label: + text: + '[color=#999999]Test[/color] kivy ' \ + '[color=#999999]keyboard[/color] modes' + height: self.texture_size[1] + size_hint_y: None + padding: 0, 20 + font_size: sp(FONT_SIZE_TITLE) + font_name: 'static/Blanka-Regular.otf' + text_size: root.width, None + markup: True + halign: 'center' + Label: + text: + 'Specifies the behavior of window contents on display ' \ + 'of the soft keyboard on Android.\n\n\n\n' \ + '[color=#ff5900]WARNING:[/color] ' \ + 'these tests only works on an Android device' + markup: True + padding: 0, 20 + size_hint_y: None + text_size: root.width, None + font_size: sp(FONT_SIZE_TEXT) + height: self.texture_size[1] + halign: 'center' + Spacer20: + Spacer20: + RelativeLayout: + size_hint_y: None + height: dp(50) + BoxLayout: + size_hint_x: None + width: min(dp(500), root.width) + orientation: 'horizontal' + pos_hint: {'center_x': .5} + ToggleButton: + text: 'None' + group: 'keyboard_modes' + state: 'down' + on_press: Window.softinput_mode = '' + ToggleButton: + text: 'pan' + group: 'keyboard_modes' + on_press: Window.softinput_mode = 'pan' + ToggleButton: + text: 'below_target' + group: 'keyboard_modes' + on_press: Window.softinput_mode = 'below_target' + ToggleButton: + text: 'resize' + group: 'keyboard_modes' + on_press: Window.softinput_mode = 'resize' + Widget: + Scatter: + id: scatter + size_hint: None, None + size: dp(300), dp(80) + on_parent: self.pos = (300, 100) + BoxLayout: + size: scatter.size + orientation: 'horizontal' + canvas: + Color: + rgba: 1, 0, 1, .25 + Rectangle: + pos: 0, 0 + size: self.size + Label: + size_hint_x: None + width: dp(30) + text: 'drag me' + canvas.before: + Color: + rgb: 1, 1, 1 + PushMatrix + Translate: + xy: self.center_x, self.center_y + Rotate: + angle: 90 + axis: 0, 0, 1 + Translate: + xy: -self.center_x, -self.center_y + canvas.after: + PopMatrix + TextInput: + text: 'type in me' diff --git a/testapps/on_device_unit_tests/test_app/screen_orientation.kv b/testapps/on_device_unit_tests/test_app/screen_orientation.kv new file mode 100644 index 0000000000..aa0bdfb56e --- /dev/null +++ b/testapps/on_device_unit_tests/test_app/screen_orientation.kv @@ -0,0 +1,74 @@ +#:import FONT_SIZE_SUBTITLE constants.FONT_SIZE_SUBTITLE +#:import FONT_SIZE_TEXT constants.FONT_SIZE_TEXT +#:import FONT_SIZE_TITLE constants.FONT_SIZE_TITLE + +#:import set_device_orientation tools.set_device_orientation +#:import Spacer20 widgets.Spacer20 + +: + name: 'orientation' + ScrollView: + BoxLayout: + orientation: 'vertical' + size_hint_y: None + height: self.minimum_height + Button: + text: 'Back to unittests' + font_size: sp(FONT_SIZE_SUBTITLE) + size_hint_y: None + height: dp(60) + on_press: root.parent.current = 'unittests' + Image: + keep_ratio: False + allow_stretch: True + source: 'static/coloursinv.png' + size_hint_y: None + height: dp(100) + Label: + text: + '[color=#999999]Test[/color] device ' \ + '[color=#999999]orientation[/color]' + height: self.texture_size[1] + size_hint_y: None + padding: 0, 20 + font_size: sp(FONT_SIZE_TITLE) + font_name: 'static/Blanka-Regular.otf' + text_size: root.width, None + markup: True + halign: 'center' + Spacer20: + Spacer20: + RelativeLayout: + size_hint_y: None + height: dp(50) + BoxLayout: + size_hint_x: None + width: min(dp(500), root.width) + pos_hint: {'center_x': .5} + orientation: 'horizontal' + ToggleButton: + text: 'Sensor' + group: 'device_orientations' + state: 'down' + on_press: set_device_orientation('sensor') + ToggleButton: + text: 'Horizontal' + group: 'device_orientations' + on_press: set_device_orientation('horizontal') + ToggleButton: + text: 'Vertical' + group: 'device_orientations' + on_press: set_device_orientation('vertical') + Spacer20: + Spacer20: + Label: + text: + '[color=#ff5900]WARNING:[/color] ' \ + 'these tests only works on an Android device' + markup: True + padding: 20, 20 + size_hint_y: None + text_size: root.width, None + font_size: sp(FONT_SIZE_TEXT) + height: self.texture_size[1] + halign: 'center' diff --git a/testapps/on_device_unit_tests/test_app/screen_service.kv b/testapps/on_device_unit_tests/test_app/screen_service.kv new file mode 100644 index 0000000000..8e5398f13e --- /dev/null +++ b/testapps/on_device_unit_tests/test_app/screen_service.kv @@ -0,0 +1,67 @@ +#:import FONT_SIZE_SUBTITLE constants.FONT_SIZE_SUBTITLE +#:import FONT_SIZE_TEXT constants.FONT_SIZE_TEXT +#:import FONT_SIZE_TITLE constants.FONT_SIZE_TITLE + +#:import set_device_orientation tools.set_device_orientation +#:import Spacer20 widgets.Spacer20 +#:import CircularButton widgets.CircularButton + +#:set green_color (0.3, 0.5, 0, 1) +#:set red_color (1.0, 0, 0, 1) + +: + name: 'service' + ScrollView: + BoxLayout: + orientation: 'vertical' + size_hint_y: None + height: self.minimum_height + Button: + text: 'Back to unittests' + font_size: sp(FONT_SIZE_SUBTITLE) + size_hint_y: None + height: dp(60) + on_press: root.parent.current = 'unittests' + Image: + keep_ratio: False + allow_stretch: True + source: 'static/coloursinv.png' + size_hint_y: None + height: dp(100) + Label: + text: + '[color=#999999]Test[/color] P4A ' \ + '[color=#999999]service[/color]' + size_hint_y: None + padding: 0, 20 + height: self.texture_size[1] + halign: 'center' + font_size: sp(FONT_SIZE_TITLE) + font_name: 'static/Blanka-Regular.otf' + text_size: root.width, None + markup: True + Spacer20: + Spacer20: + RelativeLayout: + size_hint_y: None + height: dp(100) + CircularButton: + text: 'Start service' if not app.service_running else 'Stop Service' + pos_hint: {'center_x': .5} + background_color: red_color + on_press: + app.service_running = not app.service_running; + self.background_color = green_color \ + if app.service_running else red_color + Spacer20: + Spacer20: + Label: + text: + '[color=#ff5900]WARNING:[/color] ' \ + 'this test only works on an Android device' + markup: True + size_hint_y: None + height: self.texture_size[1] + halign: 'center' + text_size: root.width, None + font_size: sp(FONT_SIZE_TEXT) diff --git a/testapps/on_device_unit_tests/test_app/screen_unittests.kv b/testapps/on_device_unit_tests/test_app/screen_unittests.kv new file mode 100644 index 0000000000..b04d98fd41 --- /dev/null +++ b/testapps/on_device_unit_tests/test_app/screen_unittests.kv @@ -0,0 +1,155 @@ +#:import sys sys + +#:import Clock kivy.clock.Clock +#:import Metrics kivy.metrics.Metrics + +#:import FONT_SIZE_SUBTITLE constants.FONT_SIZE_SUBTITLE +#:import FONT_SIZE_TEXT constants.FONT_SIZE_TEXT +#:import FONT_SIZE_TITLE constants.FONT_SIZE_TITLE +#:import Spacer20 widgets.Spacer20 + +: + name: 'unittests' + ScrollView: + id: scroll_view + GridLayout: + id: grid + cols: 1 + size_hint_y: None + height: self.minimum_height + BoxLayout: + id: header_box + orientation: 'vertical' + size_hint_y: None + height: self.minimum_height + GridLayout: + rows: 1 if root.width > root.height else 2 + size_hint_y: None + height: dp(60) * self.rows + Button: + text: 'Test vibration' + font_size: sp(FONT_SIZE_SUBTITLE) + on_press: app.test_vibration_with_pyjnius() + Button: + text: 'Test Keyboard' + font_size: sp(FONT_SIZE_SUBTITLE) + on_press: root.parent.current = 'keyboard' + Button: + text: 'Test Orientation' + font_size: sp(FONT_SIZE_SUBTITLE) + on_press: root.parent.current = 'orientation' + Button: + text: 'Test Service' + font_size: sp(FONT_SIZE_SUBTITLE) + on_press: root.parent.current = 'service' + Image: + keep_ratio: False + allow_stretch: True + source: 'static/colours.png' + size_hint_y: None + height: dp(100) + Label: + height: self.texture_size[1] + size_hint_y: None + padding: 0, 20 + font_name: 'static/Blanka-Regular.otf' + font_size: sp(FONT_SIZE_TITLE) + text_size: self.size[0], None + markup: True + text: + '[color=#999999]Kivy[/color] on ' \ + '[color=#999999]SDL2[/color] on ' \ + '[color=#999999]Android[/color] !' + halign: 'center' + Label: + height: self.texture_size[1] + size_hint_y: None + text_size: self.size[0], None + font_size: sp(FONT_SIZE_TEXT) + markup: True + text: sys.version + halign: 'center' + padding_y: dp(10) + Spacer20: + Label: + height: self.texture_size[1] + size_hint_y: None + font_size: sp(FONT_SIZE_SUBTITLE) + text_size: self.size[0], None + markup: True + text: + 'Dpi: {}\nDensity: {}\nFontscale: {}'.format( + Metrics.dpi, Metrics.density, Metrics.fontscale) + halign: 'center' + Spacer20: + BoxLayout: + id: output_box + orientation: 'vertical' + size_hint_y: None + height: self.minimum_height + canvas.before: + Color: + rgba: 1, 0, 1, .25 + Rectangle: + pos: self.pos + size: self.size + Spacer20: + Label: + id: test_packages_text + size_hint_y: None + text_size: self.width, None + height: self.texture_size[1] + font_size: sp(FONT_SIZE_SUBTITLE) + padding: 40, 20 + markup: True + text: app.test_packages + canvas.before: + Color: + rgba: 0, 0, 0, .65 + Rectangle: + pos: self.x + 20, self.y + size: self.width - 40, self.height + Label: + id: output_text + height: self.texture_size[1] + size_hint: None, None + pos_hint: {'center_x': .5 } + width: output_box.width - 40 + padding: 20, 20 + font_size: sp(FONT_SIZE_TEXT) + text_size: self.size[0], None + markup: True + text: app.unittest_error_text + halign: 'justify' + canvas.before: + Color: + rgba: 0, 0, 0, .35 + Rectangle: + pos: self.pos + size: self.size + Widget: + id: fill_space + size_hint_y: None + height: + max(20, root.height - header_box.height - output_box.height + 20) + canvas.before: + Color: + rgba: 1, 0, 1, .25 + Rectangle: + pos: self.pos + size: self.size + BoxLayout: + id: test_images_box + orientation: 'vertical' + size_hint_y: None + height: self.minimum_height if self.children else 0 + padding: 0, 20 + Button: + size_hint_y: None + height: dp(60) + text: 'Restart unittests' + font_size: sp(FONT_SIZE_SUBTITLE) + on_press: + app.reset_unittests_results(refresh_ui=True); + root.ids.scroll_view.scroll_y = 1; + Clock.schedule_once(app.run_unittests, 2) diff --git a/testapps/on_device_unit_tests/test_app/static/Blanka-Regular.otf b/testapps/on_device_unit_tests/test_app/static/Blanka-Regular.otf new file mode 100644 index 0000000000..60b18d7816 Binary files /dev/null and b/testapps/on_device_unit_tests/test_app/static/Blanka-Regular.otf differ diff --git a/testapps/testapp/colours.png b/testapps/on_device_unit_tests/test_app/static/colours.png similarity index 100% rename from testapps/testapp/colours.png rename to testapps/on_device_unit_tests/test_app/static/colours.png diff --git a/testapps/testapp_flask/static/coloursinv.png b/testapps/on_device_unit_tests/test_app/static/coloursinv.png similarity index 100% rename from testapps/testapp_flask/static/coloursinv.png rename to testapps/on_device_unit_tests/test_app/static/coloursinv.png diff --git a/testapps/on_device_unit_tests/test_app/static/flask.css b/testapps/on_device_unit_tests/test_app/static/flask.css new file mode 100644 index 0000000000..80a9e88807 --- /dev/null +++ b/testapps/on_device_unit_tests/test_app/static/flask.css @@ -0,0 +1,133 @@ + +@font-face{ + font-family: Blanka; + src: url('Blanka-Regular.otf'); + font-weight: normal; +} + +body { + margin: 0; + padding: 0; + font-family: "Roboto Slab", Roboto, sans-serif; + color: #444; +} + +/* + * Formatting the header area + */ +header { + background-color: #6600b8; + height: 90px; + width: 100%; + opacity: .9; + margin-bottom: 10px; +} +header h1.logo { + margin-top: 4px; + font-size: 1.4em; + font-family: Blanka, sans-serif; + color: #fff; + text-transform: uppercase; + float: left; + text-align: center; + width: 100%; +} +header h1.logo:hover { + color: #000000; + text-decoration: none; +} + +/* + * Navbar + */ +.menu { + float: right; + margin-top: 0px; + width: 100%; + text-align: center; + font-family: Blanka, sans-serif; +} +.menu li { + display: inline-block; +} +.menu li + li { + margin-left: 35px; +} +.menu li a { + color: #999999; + text-decoration: none; + text-transform: uppercase; +} + +/* + * Centering the body content + */ +.container { + width: auto; + margin: 0 8px; + text-align: center; +} + +h2.page-title { + text-align: center; + text-transform: uppercase; + font-family: Blanka, sans-serif; +} + +.text-underline { + border-bottom: 0px solid #333; + font-family: Blanka, sans-serif; + width: 100%; + display: block; +} + +.center { + margin: auto; + width: 50%; +} + +/* + * Unittests page + */ +.text-green { + color: #5d8000; +} + +.text-red { + color: #ff0000; +} + +.terminal-box { + background-color: #333; +} + +.terminal-content { + color: #fff; + margin: 8px; + padding-top: 8px; +} + +/* + * Adapt header to bigger screens + */ +@media only screen and (min-width: 530px) { + header { + height: 45px; + } + + header h1.logo { + width: auto; + text-align: left; + } + + .menu { + width: auto; + margin-top: 12px; + } + .container { + text-align: left; + } + .text-underline { + border-bottom: 1px solid #333; + } +} \ No newline at end of file diff --git a/testapps/on_device_unit_tests/test_app/templates/base.html b/testapps/on_device_unit_tests/test_app/templates/base.html new file mode 100644 index 0000000000..50d3383e56 --- /dev/null +++ b/testapps/on_device_unit_tests/test_app/templates/base.html @@ -0,0 +1,35 @@ + + + + + + + + {% block title %} + Flask on {{ platform }} + {% endblock %} + + + + +
+
+

Flask on {{ platform }}!

+ + + +
+
+ +
+ {% block body %} + {% endblock %} +
+ + + diff --git a/testapps/on_device_unit_tests/test_app/templates/index.html b/testapps/on_device_unit_tests/test_app/templates/index.html new file mode 100644 index 0000000000..797724e541 --- /dev/null +++ b/testapps/on_device_unit_tests/test_app/templates/index.html @@ -0,0 +1,97 @@ +{% extends "base.html" %} + + +{% block body %} + +

Main Page

+ + + +
+

Perform unittests

+
+ + +
+
+ +
+

Test navigation

+
+ +
+
+ +
+

Android tests

+ +
+ + + +
+ +
+ + + + +
+ +
+ + + + + + + +
+
+
+
+ +{% endblock %} diff --git a/testapps/on_device_unit_tests/test_app/templates/page2.html b/testapps/on_device_unit_tests/test_app/templates/page2.html new file mode 100644 index 0000000000..f2fd122b78 --- /dev/null +++ b/testapps/on_device_unit_tests/test_app/templates/page2.html @@ -0,0 +1,22 @@ + +{% extends "base.html" %} + + +{% block body %} + +

Page two

+ + + +
+
+ Yeah, it seems to work, I would suggest to go to: +
+
+ +
+
+ ...far more interesting ;) +
+ +{% endblock %} diff --git a/testapps/on_device_unit_tests/test_app/templates/unittests.html b/testapps/on_device_unit_tests/test_app/templates/unittests.html new file mode 100644 index 0000000000..95b23995e8 --- /dev/null +++ b/testapps/on_device_unit_tests/test_app/templates/unittests.html @@ -0,0 +1,20 @@ +{% extends "base.html" %} + + +{% block body %} + +

Unittests Page

+ +
+Unittest recipes: {{ tested_recipes }} +
+ +
+
+ {% for line in unittests_output %} + {{ line }}
+ {% endfor %} +
+
+ +{% endblock %} \ No newline at end of file diff --git a/testapps/on_device_unit_tests/test_app/tests/mixin.py b/testapps/on_device_unit_tests/test_app/tests/mixin.py new file mode 100644 index 0000000000..af10dae693 --- /dev/null +++ b/testapps/on_device_unit_tests/test_app/tests/mixin.py @@ -0,0 +1,30 @@ +import importlib + + +class PythonTestMixIn(object): + + module_import = None + + def test_import_module(self): + """Test importing the specified Python module name. This import test + is common to all Python modules, it does not test any further + functionality. + """ + self.assertIsNotNone( + self.module_import, + 'module_import is not set (was default None)') + + importlib.import_module(self.module_import) + + def test_run_module(self): + """Import the specified module and do something with it as a minimal + check that it actually works. + + This test fails by default, it must be overridden by every + child test class. + """ + + self.fail('This test must be overridden by {}'.format(self)) + + +print('Defined test case') diff --git a/testapps/on_device_unit_tests/test_app/tests/pandas_test_data.csv b/testapps/on_device_unit_tests/test_app/tests/pandas_test_data.csv new file mode 100644 index 0000000000..d329f122e3 --- /dev/null +++ b/testapps/on_device_unit_tests/test_app/tests/pandas_test_data.csv @@ -0,0 +1,21 @@ +Date,Open,High,Low,Close,Volume +11/1/2019,3050.72,3066.95,3050.72,3066.91,510301237 +11/4/2019,3078.96,3085.2,3074.87,3078.27,524848878 +11/5/2019,3080.8,3083.95,3072.15,3074.62,585634570 +11/6/2019,3075.1,3078.34,3065.89,3076.78,544288522 +11/7/2019,3087.02,3097.77,3080.23,3085.18,566117910 +11/8/2019,3081.25,3093.09,3073.58,3093.08,460757054 +11/11/2019,3080.33,3088.33,3075.82,3087.01,366044400 +11/12/2019,3089.28,3102.61,3084.73,3091.84,434953689 +11/13/2019,3084.18,3098.06,3078.8,3094.04,454607412 +11/14/2019,3090.75,3098.2,3083.26,3096.63,408390424 +11/15/2019,3107.92,3120.46,3104.6,3120.46,579104868 +11/18/2019,3117.91,3124.17,3112.06,3122.03,521730492 +11/19/2019,3127.45,3127.64,3113.47,3120.18,513153035 +11/20/2019,3114.66,3118.97,3091.41,3108.46,756408988 +11/21/2019,3108.49,3110.11,3094.55,3103.54,476836171 +11/22/2019,3111.41,3112.87,3099.26,3110.29,418027927 +11/25/2019,3117.44,3133.83,3117.44,3133.64,513728761 +11/26/2019,3134.85,3142.69,3131,3140.52,986041660 +11/27/2019,3145.49,3154.26,3143.41,3153.63,421853938 +11/29/2019,3147.18,3150.3,3139.34,3140.98,286602291 diff --git a/testapps/on_device_unit_tests/test_app/tests/test_requirements.py b/testapps/on_device_unit_tests/test_app/tests/test_requirements.py index 625a99e5db..a4246c12d4 100644 --- a/testapps/on_device_unit_tests/test_app/tests/test_requirements.py +++ b/testapps/on_device_unit_tests/test_app/tests/test_requirements.py @@ -1,6 +1,6 @@ -from main import PythonTestMixIn from unittest import TestCase +from .mixin import PythonTestMixIn class NumpyTestCase(PythonTestMixIn, TestCase): @@ -21,9 +21,9 @@ def test_run_module(self): ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) ctx.options &= ~ssl.OP_NO_SSLv3 - -class SqliteTestCase(PythonTestMixIn, TestCase): + +class Sqlite3TestCase(PythonTestMixIn, TestCase): module_import = 'sqlite3' def test_run_module(self): @@ -31,7 +31,7 @@ def test_run_module(self): conn = sqlite3.connect('example.db') conn.cursor() - + class KivyTestCase(PythonTestMixIn, TestCase): module_import = 'kivy' @@ -47,4 +47,232 @@ class PyjniusTestCase(PythonTestMixIn, TestCase): def test_run_module(self): from jnius import autoclass + autoclass('org.kivy.android.PythonActivity') + + +class LibffiTestCase(PythonTestMixIn, TestCase): + module_import = 'ctypes' + + def test_run_module(self): + from os import environ + from ctypes import cdll + + if "ANDROID_APP_PATH" in environ: + libc = cdll.LoadLibrary("libc.so") + else: + from ctypes.util import find_library + path_libc = find_library("c") + libc = cdll.LoadLibrary(path_libc) + libc.printf(b"%s\n", b"Using the C printf function from Python ... ") + + +class RequestsTestCase(PythonTestMixIn, TestCase): + module_import = 'requests' + + def test_run_module(self): + import requests + + requests.get('https://kivy.org/') + + +class PillowTestCase(PythonTestMixIn, TestCase): + module_import = 'PIL' + + def test_run_module(self): + import os + from PIL import ( + Image as PilImage, + ImageOps, + ImageFont, + ImageDraw, + ImageFilter, + ImageChops, + ) + + text_to_draw = "Kivy" + img_target = "pillow_text_draw.png" + image_width = 200 + image_height = 100 + + img = PilImage.open("static/colours.png") + img = img.resize((image_width, image_height), PilImage.ANTIALIAS) + font = ImageFont.truetype("static/Blanka-Regular.otf", 55) + + draw = ImageDraw.Draw(img) + for n in range(2, image_width, 2): + draw.rectangle( + (n, n, image_width - n, image_height - n), outline="black" + ) + img.filter(ImageFilter.GaussianBlur(radius=1.5)) + + text_pos = (image_width / 2.0 - 55, 5) + halo = PilImage.new("RGBA", img.size, (0, 0, 0, 0)) + ImageDraw.Draw(halo).text( + text_pos, text_to_draw, font=font, fill="black" + ) + blurred_halo = halo.filter(ImageFilter.BLUR) + ImageDraw.Draw(blurred_halo).text( + text_pos, text_to_draw, font=font, fill="white" + ) + img = PilImage.composite( + img, blurred_halo, ImageChops.invert(blurred_halo) + ) + + img.save(img_target, "PNG") + self.assertTrue(os.path.isfile(img_target)) + + +class KiwisolverTestCase(PythonTestMixIn, TestCase): + module_import = 'kiwisolver' + + def test_run_module(self): + from kiwisolver import Variable, Solver + + x1 = Variable('x1') + x2 = Variable('x2') + xm = Variable('xm') + constraints = [x1 >= 0, x2 <= 100, x2 >= x1 + 10, xm == (x1 + x2) / 2] + + solver = Solver() + + for cn in constraints: + solver.addConstraint(cn) + + +class PandasTestCase(PythonTestMixIn, TestCase): + module_import = 'pandas' + + def test_run_module(self): + import pandas as pd + + df = pd.read_csv( + 'tests/pandas_test_data.csv', index_col=0, parse_dates=True, + ) + df.head() + df["Volume"].mean() + df[["High", "Low"]].describe() + + +class MatplotlibTestCase(PythonTestMixIn, TestCase): + module_import = 'matplotlib' + + def test_run_module(self): + import os + import numpy as np + from matplotlib import pyplot as plt + + fig, ax = plt.subplots() + ax.set_xlabel('test xlabel') + ax.set_ylabel('test ylabel') + ax.plot(np.random.random(50)) + ax.plot(np.sin(np.linspace(0, 3 * np.pi, 30))) + + ax.legend(['random numbers', 'sin']) + + fig.set_size_inches((5, 4)) + fig.tight_layout() + + fig.savefig('matplotlib_test.png', dpi=150) + self.assertTrue(os.path.isfile("matplotlib_test.png")) + + +class MplfinanceTestCase(PythonTestMixIn, TestCase): + module_import = 'mplfinance' + + def test_run_module(self): + import pandas as pd + import mplfinance as mpf + + df = pd.read_csv( + 'tests/pandas_test_data.csv', index_col=0, parse_dates=True, + ) + mpf.plot(df, type='candle', volume=True, savefig='mplfinance_test.png') + + +class CryptographyTestCase(PythonTestMixIn, TestCase): + module_import = 'cryptography' + + def test_run_module(self): + from cryptography.fernet import Fernet + + key = Fernet.generate_key() + f = Fernet(key) + cryptography_encrypted = f.encrypt( + b'A really secret message. Not for prying eyes.') + cryptography_decrypted = f.decrypt(cryptography_encrypted) + + +class PycryptoTestCase(PythonTestMixIn, TestCase): + module_import = 'Crypto' + + def test_run_module(self): + from Crypto.Hash import SHA256 + + crypto_hash_message = 'A secret message' + hash = SHA256.new() + hash.update(crypto_hash_message) + crypto_hash_hexdigest = hash.hexdigest() + + +class PycryptodomeTestCase(PythonTestMixIn, TestCase): + module_import = 'Crypto' + + def test_run_module(self): + import os + from Crypto.PublicKey import RSA + + print('Ok imported pycryptodome, testing some basic operations...') + secret_code = "Unguessable" + key = RSA.generate(2048) + encrypted_key = key.export_key(passphrase=secret_code, pkcs=8, + protection="scryptAndAES128-CBC") + print('\t -> Testing key for secret code "Unguessable": {}'.format( + encrypted_key)) + + file_out = open("rsa_key.bin", "wb") + file_out.write(encrypted_key) + print('\t -> Testing key write: {}'.format( + 'ok' if os.path.exists("rsa_key.bin") else 'fail')) + self.assertTrue(os.path.exists("rsa_key.bin")) + + print('\t -> Testing Public key:'.format(key.publickey().export_key())) + + +class ScryptTestCase(PythonTestMixIn, TestCase): + module_import = 'scrypt' + + def test_run_module(self): + import scrypt + h1 = scrypt.hash('password', 'random salt') + # The hash should be 64 bytes (default value) + self.assertEqual(64, len(h1)) + + +class M2CryptoTestCase(PythonTestMixIn, TestCase): + module_import = 'M2Crypto' + + def test_run_module(self): + from M2Crypto import SSL + ctx = SSL.Context('sslv23') + + +class Pysha3TestCase(PythonTestMixIn, TestCase): + module_import = 'sha3' + + def test_run_module(self): + import sha3 + + print('Ok imported pysha3, testing some basic operations...') + k = sha3.keccak_512() + k.update(b"data") + print('Test pysha3 operation (keccak_512): {}'.format(k.hexdigest())) + + +class LibtorrentTestCase(PythonTestMixIn, TestCase): + module_import = 'libtorrent' + + def test_run_module(self): + import libtorrent as lt + + print('Imported libtorrent version {}'.format(lt.version)) diff --git a/testapps/on_device_unit_tests/test_app/tools.py b/testapps/on_device_unit_tests/test_app/tools.py new file mode 100644 index 0000000000..16dac01022 --- /dev/null +++ b/testapps/on_device_unit_tests/test_app/tools.py @@ -0,0 +1,162 @@ +# -*- coding: utf-8 -*- + +import glob +import unittest + +try: + # python2 case + from StringIO import StringIO +except ImportError: + # python3 case + from io import StringIO +from os.path import abspath, split, join + +from constants import RUNNING_ON_ANDROID + +APP_PATH = split(abspath(__file__))[0] + + +def run_test_suites_into_buffer(suites): + """Run a suite of unittests but into a buffer so we can read the result.""" + terminal_output = StringIO() + unittest.TextTestRunner(stream=terminal_output).run(suites) + return terminal_output.getvalue() + + +def get_images_with_extension(path=APP_PATH, extension='*.png'): + """ + Return a list of image files given a path and an file extension. + + .. note:: those image files are supposed to be created by our unittests + inside the app's root folder. + """ + return glob.glob(join(path, extension)) + + +def load_kv_from(kv_name): + """ + Load a kivy's kv file givel a kv filename. + + .. note:: requires `.kv` extension. + """ + from kivy.lang import Builder + + kv_file = join(APP_PATH, kv_name) + return Builder.load_file(kv_file) + + +def raise_error(error): + """ + A function to notify an error without raising an exception. + + .. warning:: we will try to notify via an kivy's Popup, but if kivy is not + installed, it will only print an error message. + """ + try: + from widgets import ErrorPopup + except ImportError: + print('raise_error:', error) + return + ErrorPopup(error_text=error).open() + + +def get_failed_unittests_from(unittests_output, set_of_tests): + """Parse unittests output trying to find the failed tests""" + failed_tests = set() + for test in set_of_tests: + if test in unittests_output: + failed_tests.add(test) + return failed_tests + + +def skip_if_not_running_from_android_device(func): + """ + Skip run of the function in case that we are running the app form android. + + .. note:: this is useful for some kind of tests that are supposed to be run + from an android device and relies on `pyjnius`. + """ + + def wrapper(*arg, **kwarg): + if RUNNING_ON_ANDROID: + return func(*arg, **kwarg) + raise_error( + 'Function `{func_name}` only available for android devices'.format( + func_name=func.__name__, + ), + ) + return None + + return wrapper + + +@skip_if_not_running_from_android_device +def get_android_python_activity(): + """ + Return the `PythonActivity.mActivity` using `pyjnius`. + + .. warning:: This function will only be ran if executed from android""" + from jnius import autoclass + + PythonActivity = autoclass('org.kivy.android.PythonActivity') + return PythonActivity.mActivity + + +@skip_if_not_running_from_android_device +def vibrate_with_pyjnius(time=1000): + """ + Vibrate an android device using `pyjnius`. + + .. warning:: This function will only be ran if executed from android.""" + from jnius import autoclass, cast + + print('Attempting to vibrate with pyjnius') + ANDROID_VERSION = autoclass('android.os.Build$VERSION') + SDK_INT = ANDROID_VERSION.SDK_INT + + Context = autoclass("android.content.Context") + activity = get_android_python_activity() + + vibrator_service = activity.getSystemService(Context.VIBRATOR_SERVICE) + vibrator = cast("android.os.Vibrator", vibrator_service) + + if vibrator and SDK_INT >= 26: + print("Using android's `VibrationEffect` (SDK >= 26)") + VibrationEffect = autoclass("android.os.VibrationEffect") + vibrator.vibrate( + VibrationEffect.createOneShot( + time, VibrationEffect.DEFAULT_AMPLITUDE, + ), + ) + elif vibrator: + print("Using deprecated android's vibrate (SDK < 26)") + vibrator.vibrate(time) + else: + print('Something happened...vibrator service disabled?') + + +@skip_if_not_running_from_android_device +def set_device_orientation(direction): + """ + Modifies the app orientation for an android device. + + .. warning:: This function will only be ran if executed from android.""" + if direction not in ('sensor', 'horizontal', 'vertical'): + print( + 'ERROR: asked to orient to `{direction}`, but we only support: ' + 'sensor, horizontal or vertical'.format(direction=direction) + ) + from jnius import autoclass + + activity = get_android_python_activity() + ActivityInfo = autoclass('android.content.pm.ActivityInfo') + + if direction == 'sensor': + activity.setRequestedOrientation( + ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) + elif direction == 'horizontal': + activity.setRequestedOrientation( + ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) + else: + activity.setRequestedOrientation( + ActivityInfo.SCREEN_ORIENTATION_PORTRAIT) diff --git a/testapps/on_device_unit_tests/test_app/widgets.kv b/testapps/on_device_unit_tests/test_app/widgets.kv new file mode 100644 index 0000000000..997e81000e --- /dev/null +++ b/testapps/on_device_unit_tests/test_app/widgets.kv @@ -0,0 +1,84 @@ +#:import get_color_from_hex kivy.utils.get_color_from_hex +#:import FONT_SIZE_SUBTITLE constants.FONT_SIZE_SUBTITLE + +: + size_hint_y: None + height: dp(20) + + +: + text: '' + source: '' + size_hint_y: None + height: self.minimum_height + orientation: 'vertical' + canvas.before: + Color: + rgba: 1, 0, 1, .25 + Rectangle: + pos: self.pos + size: self.size + Spacer20: + Label: + text: root.text + size_hint_y: None + height: dp(60) + font_size: sp(FONT_SIZE_SUBTITLE) + canvas.before: + Color: + rgba: 0, 0, 0, .65 + Rectangle: + pos: self.x + 20, self.y + size: self.width - 40, self.height + BoxLayout: + size_hint_y: None + height: self.minimum_height + orientation: 'vertical' + canvas.before: + Color: + rgba: 0, 0, 0, .35 + Rectangle: + pos: self.x + 20, self.y + size: self.width - 40, self.height + Spacer20: + Image: + source: root.source + allow_stretch: True + size_hint_y: None + height: dp(120) + Spacer20: + Spacer20: + + +: + size_hint: None, None + size: dp(120), dp(120) + text: '' + background_color: None + canvas.before: + Color: + rgba: .34, .34, .34, 1 + Ellipse: + pos: self.pos + size: self.size + canvas: + Color: + rgba: + root.background_color \ + if root.background_color \ + else (1., 0., 1., .65) # purple + Ellipse: + pos: self.x + dp(2), self.y + dp(2) + size: self.width - dp(4), self.height - dp(4) + Label: + text: root.text + pos: root.pos + size_hint: None, None + size: root.size + + +: + title: 'Error' + size_hint: 0.75, 0.75 + Label: + text: root.error_text \ No newline at end of file diff --git a/testapps/on_device_unit_tests/test_app/widgets.py b/testapps/on_device_unit_tests/test_app/widgets.py new file mode 100644 index 0000000000..eab93405ae --- /dev/null +++ b/testapps/on_device_unit_tests/test_app/widgets.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- + +from kivy.properties import StringProperty +from kivy.uix.boxlayout import BoxLayout +from kivy.uix.popup import Popup +from kivy.uix.behaviors.button import ButtonBehavior +from kivy.uix.widget import Widget +from kivy.vector import Vector +from tools import load_kv_from + +load_kv_from('widgets.kv') + + +class Spacer20(Widget): + pass + + +class TestImage(BoxLayout): + text = StringProperty() + source = StringProperty() + + +class CircularButton(ButtonBehavior, Widget): + def collide_point(self, x, y): + return Vector(x, y).distance(self.center) <= self.width / 2 + + +class ErrorPopup(Popup): + error_text = StringProperty('') diff --git a/testapps/setup_keyboard.py b/testapps/setup_keyboard.py deleted file mode 100644 index 26499a639a..0000000000 --- a/testapps/setup_keyboard.py +++ /dev/null @@ -1,32 +0,0 @@ - -from distutils.core import setup -from setuptools import find_packages - -options = {'apk': {'debug': None, - 'requirements': 'sdl2,pyjnius,kivy,python3', - 'blacklist-requirements': 'openssl,sqlite3', - 'android-api': 27, - 'ndk-api': 21, - 'ndk-dir': '/home/asandy/android/android-ndk-r17c', - 'dist-name': 'bdisttest', - 'ndk-version': '10.3.2', - 'permission': 'VIBRATE', - }} - -package_data = {'': ['*.py', - '*.png'] - } - -packages = find_packages() -print('packages are', packages) - -setup( - name='testapp_keyboard', - version='1.1', - description='p4a setup.py test', - author='Alexander Taylor', - author_email='alexanderjohntaylor@gmail.com', - packages=find_packages(), - options=options, - package_data={'setup_keyboard': ['*.py', '*.png']} -) diff --git a/testapps/setup_testapp_flask.py b/testapps/setup_testapp_flask.py deleted file mode 100644 index 3b2536e579..0000000000 --- a/testapps/setup_testapp_flask.py +++ /dev/null @@ -1,37 +0,0 @@ - -from distutils.core import setup -from setuptools import find_packages - -options = {'apk': {'debug': None, - 'requirements': 'python3,flask,pyjnius', - 'blacklist-requirements': 'openssl,sqlite3', - 'android-api': 27, - 'ndk-api': 21, - 'ndk-dir': '/home/asandy/android/android-ndk-r17c', - 'dist-name': 'testapp_flask', - 'ndk-version': '10.3.2', - 'bootstrap': 'webview', - 'permissions': ['INTERNET', 'VIBRATE'], - 'arch': 'armeabi-v7a', - 'window': None, - }} - -package_data = {'': ['*.py', - '*.png'] - } - -packages = find_packages() -print('packages are', packages) - -setup( - name='testapp_flask', - version='1.0', - description='p4a flask testapp', - author='Alexander Taylor', - author_email='alexanderjohntaylor@gmail.com', - packages=find_packages(), - options=options, - package_data={'testapp_flask': ['*.py', '*.png'], - 'testapp_flask/static': ['*.png', '*.css'], - 'testapp_flask/templates': ['*.html']} -) diff --git a/testapps/setup_testapp_python2.py b/testapps/setup_testapp_python2.py deleted file mode 100644 index 9499c80c73..0000000000 --- a/testapps/setup_testapp_python2.py +++ /dev/null @@ -1,32 +0,0 @@ - -from distutils.core import setup -from setuptools import find_packages - -options = {'apk': {'requirements': 'sdl2,numpy,pyjnius,kivy,python2', - 'android-api': 27, - 'ndk-api': 21, - 'ndk-dir': '/home/asandy/android/android-ndk-r17c', - 'dist-name': 'bdisttest_python2', - 'ndk-version': '10.3.2', - 'permission': 'VIBRATE', - 'arch': 'armeabi-v7a', - 'window': None, - }} - -package_data = {'': ['*.py', - '*.png'] - } - -packages = find_packages() -print('packages are', packages) - -setup( - name='testapp_python2', - version='1.1', - description='p4a setup.py test', - author='Alexander Taylor', - author_email='alexanderjohntaylor@gmail.com', - packages=find_packages(), - options=options, - package_data={'testapp': ['*.py', '*.png']} -) diff --git a/testapps/setup_testapp_python2_sqlite_openssl.py b/testapps/setup_testapp_python2_sqlite_openssl.py deleted file mode 100644 index c1dcf53efc..0000000000 --- a/testapps/setup_testapp_python2_sqlite_openssl.py +++ /dev/null @@ -1,25 +0,0 @@ - -from distutils.core import setup -from setuptools import find_packages - -options = {'apk': {'requirements': 'sdl2,pyjnius,kivy,python2,openssl,requests,peewee,sqlite3', - 'android-api': 27, - 'ndk-api': 21, - 'ndk-dir': '/home/sandy/android/android-ndk-r17c', - 'dist-name': 'bdisttest_python2_sqlite_openssl', - 'ndk-version': '10.3.2', - 'permissions': ['INTERNET', 'VIBRATE'], - 'arch': 'armeabi-v7a', - 'window': None, - }} - -setup( - name='testapp_python2_sqlite_openssl', - version='1.1', - description='p4a setup.py test', - author='Alexander Taylor', - author_email='alexanderjohntaylor@gmail.com', - packages=find_packages(), - options=options, - package_data={'testapp_sqlite_openssl': ['*.py', '*.png']} -) diff --git a/testapps/setup_testapp_python3.py b/testapps/setup_testapp_python3.py deleted file mode 100644 index 57065941cf..0000000000 --- a/testapps/setup_testapp_python3.py +++ /dev/null @@ -1,30 +0,0 @@ - -from distutils.core import setup -from setuptools import find_packages - -options = {'apk': {'requirements': 'sdl2,numpy,pyjnius,kivy,python3', - 'blacklist-requirements': 'openssl,sqlite3', - 'android-api': 27, - 'ndk-api': 21, - 'dist-name': 'bdisttest_python3_googlendk', - 'ndk-version': '10.3.2', - 'permission': 'VIBRATE', - }} - -package_data = {'': ['*.py', - '*.png'] - } - -packages = find_packages() -print('packages are', packages) - -setup( - name='python3_googlendk', - version='1.1', - description='p4a setup.py test', - author='Alexander Taylor', - author_email='alexanderjohntaylor@gmail.com', - packages=find_packages(), - options=options, - package_data={'testapp': ['*.py', '*.png']} -) diff --git a/testapps/setup_testapp_python3_matplotlib.py b/testapps/setup_testapp_python3_matplotlib.py deleted file mode 100644 index 014f4492b2..0000000000 --- a/testapps/setup_testapp_python3_matplotlib.py +++ /dev/null @@ -1,30 +0,0 @@ - -from distutils.core import setup -from setuptools import find_packages - -options = {'apk': {'requirements': 'sdl2,python3,matplotlib,pyparsing,cycler,python-dateutil,numpy,kiwisolver,kivy', - 'blacklist-requirements': 'openssl,sqlite3', - 'android-api': 27, - 'ndk-api': 21, - 'dist-name': 'matplotlib_testapp', - 'ndk-version': '10.3.2', - 'permission': 'VIBRATE', - }} - -package_data = {'': ['*.py', - '*.png'] - } - -packages = find_packages() -print('packages are', packages) - -setup( - name='matplotlib_testapp', - version='0.1', - description='p4a setup.py test', - author='Alexander Taylor', - author_email='alexanderjohntaylor@gmail.com', - packages=find_packages(), - options=options, - package_data={'testapp_matplotlib': ['*.py', '*.png']} -) diff --git a/testapps/setup_testapp_python3_pillow.py b/testapps/setup_testapp_python3_pillow.py deleted file mode 100644 index a13c6af7e8..0000000000 --- a/testapps/setup_testapp_python3_pillow.py +++ /dev/null @@ -1,27 +0,0 @@ - -from distutils.core import setup -from setuptools import find_packages - -options = {'apk': {'requirements': 'harfbuzz,sdl2,pillow,kivy,python3', - 'blacklist-requirements': 'sqlite3', - 'android-api': 27, - 'ndk-api': 21, - 'dist-name': 'pillow_testapp', - 'arch': 'armeabi-v7a', - 'permissions': ['VIBRATE'], - }} - -package_data = {'': ['*.py', - '*.png'] - } - -setup( - name='testapp_pillow', - version='1.0', - description='p4a setup.py test', - author='Pol Canelles', - author_email='canellestudi@gmail.com', - packages=find_packages(), - options=options, - package_data={'testapp_pillow': ['*.py', '*.png', '*.ttf']} -) diff --git a/testapps/setup_testapp_python3_sqlite_openssl.py b/testapps/setup_testapp_python3_sqlite_openssl.py deleted file mode 100644 index 0f7485d132..0000000000 --- a/testapps/setup_testapp_python3_sqlite_openssl.py +++ /dev/null @@ -1,23 +0,0 @@ - -from distutils.core import setup -from setuptools import find_packages - -options = {'apk': {'requirements': 'requests,peewee,sdl2,pyjnius,kivy,python3', - 'android-api': 27, - 'ndk-api': 21, - 'dist-name': 'bdisttest_python3_sqlite_openssl_googlendk', - 'ndk-version': '10.3.2', - 'arch': 'armeabi-v7a', - 'permissions': ['INTERNET', 'VIBRATE'], - }} - -setup( - name='testapp_python3_sqlite_openssl_googlendk', - version='1.1', - description='p4a setup.py test', - author='Alexander Taylor', - author_email='alexanderjohntaylor@gmail.com', - packages=find_packages(), - options=options, - package_data={'testapp_sqlite_openssl': ['*.py', '*.png']} -) diff --git a/testapps/setup_testapp_python_encryption.py b/testapps/setup_testapp_python_encryption.py deleted file mode 100644 index 2a468ade86..0000000000 --- a/testapps/setup_testapp_python_encryption.py +++ /dev/null @@ -1,30 +0,0 @@ - -from distutils.core import setup -from setuptools import find_packages - -options = {'apk': {'requirements': 'sdl2,pyjnius,kivy,python3,cryptography,' - 'pycrypto,scrypt,m2crypto,pysha3,' - 'pycryptodome,libtorrent', - 'blacklist-requirements': 'sqlite3', - 'android-api': 27, - 'ndk-api': 21, - 'dist-name': 'bdisttest_encryption', - 'ndk-version': '10.3.2', - 'arch': 'armeabi-v7a', - 'permissions': ['INTERNET', 'VIBRATE'], - }} - -package_data = {'': ['*.py', - '*.png'] - } - -setup( - name='testapp_encryption', - version='1.0', - description='p4a setup.py test', - author='Pol Canelles', - author_email='canellestudi@gmail.com', - packages=find_packages(), - options=options, - package_data={'testapp_encryption': ['*.py', '*.png']} -) diff --git a/testapps/setup_testapp_service.py b/testapps/setup_testapp_service.py deleted file mode 100644 index a3342d43a6..0000000000 --- a/testapps/setup_testapp_service.py +++ /dev/null @@ -1,34 +0,0 @@ - -from distutils.core import setup -from setuptools import find_packages - -options = {'apk': {'debug': None, - 'requirements': 'python3,genericndkbuild,pyjnius', - 'blacklist-requirements': 'openssl,sqlite3', - 'android-api': 27, - 'ndk-api': 21, - 'sdk-dir':'/opt/android/android-sdk/', - 'ndk-dir':'/opt/android/android-ndk-r17c/', - 'dist-name': 'testapp_service', - 'ndk-version': '10.3.2', - 'bootstrap': 'service_only', - 'permissions': ['INTERNET', 'VIBRATE'], - 'arch': 'armeabi-v7a', - 'service': 'time:p4atime.py', - }} - -package_data = {'': ['*.py']} - -packages = find_packages() -print('packages are', packages) - -setup( - name='testapp_service', - version='1.1', - description='p4a service testapp', - author='Alexander Taylor', - author_email='alexanderjohntaylor@gmail.com', - packages=find_packages(), - options=options, - package_data={'testapp_service': ['*.py']} -) diff --git a/testapps/testapp/main.py b/testapps/testapp/main.py deleted file mode 100644 index 2148e7c68a..0000000000 --- a/testapps/testapp/main.py +++ /dev/null @@ -1,166 +0,0 @@ -print('main.py was successfully called') - -import os -print('imported os') - - -print('this dir is', os.path.abspath(os.curdir)) - -print('contents of this dir', os.listdir('./')) - -import sys -print('pythonpath is', sys.path) - -import kivy -print('imported kivy') -print('file is', kivy.__file__) - -from kivy.app import App - -from kivy.lang import Builder -from kivy.properties import StringProperty - -from kivy.uix.popup import Popup -from kivy.clock import Clock - -print('Imported kivy') -from kivy.utils import platform -print('platform is', platform) - -kv = ''' -#:import Metrics kivy.metrics.Metrics -#:import sys sys - -: - size_hint_y: None - height: dp(60) - - -ScrollView: - GridLayout: - cols: 1 - size_hint_y: None - height: self.minimum_height - FixedSizeButton: - text: 'test pyjnius' - on_press: app.test_pyjnius() - Image: - keep_ratio: False - allow_stretch: True - source: 'colours.png' - size_hint_y: None - height: dp(100) - Label: - height: self.texture_size[1] - size_hint_y: None - font_size: 100 - text_size: self.size[0], None - markup: True - text: '[b]Kivy[/b] on [b]SDL2[/b] on [b]Android[/b]!' - halign: 'center' - Label: - height: self.texture_size[1] - size_hint_y: None - text_size: self.size[0], None - markup: True - text: sys.version - halign: 'center' - padding_y: dp(10) - Widget: - size_hint_y: None - height: 20 - Label: - height: self.texture_size[1] - size_hint_y: None - font_size: 50 - text_size: self.size[0], None - markup: True - text: 'dpi: {}\\ndensity: {}\\nfontscale: {}'.format(Metrics.dpi, Metrics.density, Metrics.fontscale) - halign: 'center' - FixedSizeButton: - text: 'test ctypes' - on_press: app.test_ctypes() - FixedSizeButton: - text: 'test numpy' - on_press: app.test_numpy() - Widget: - size_hint_y: None - height: 1000 - on_touch_down: print('touched at', args[-1].pos) - -: - title: 'Error' - size_hint: 0.75, 0.75 - Label: - text: root.error_text -''' - - -class ErrorPopup(Popup): - error_text = StringProperty('') - -def raise_error(error): - print('ERROR:', error) - ErrorPopup(error_text=error).open() - -class TestApp(App): - def build(self): - root = Builder.load_string(kv) - Clock.schedule_interval(self.print_something, 2) - # Clock.schedule_interval(self.test_pyjnius, 5) - print('testing metrics') - from kivy.metrics import Metrics - print('dpi is', Metrics.dpi) - print('density is', Metrics.density) - print('fontscale is', Metrics.fontscale) - return root - - def print_something(self, *args): - print('App print tick', Clock.get_boottime()) - - def on_pause(self): - return True - - def test_pyjnius(self, *args): - try: - from jnius import autoclass, cast - except ImportError: - raise_error('Could not import pyjnius') - return - print('Attempting to vibrate with pyjnius') - ANDROID_VERSION = autoclass('android.os.Build$VERSION') - SDK_INT = ANDROID_VERSION.SDK_INT - - Context = autoclass("android.content.Context") - PythonActivity = autoclass('org.kivy.android.PythonActivity') - activity = PythonActivity.mActivity - - vibrator_service = activity.getSystemService(Context.VIBRATOR_SERVICE) - vibrator = cast("android.os.Vibrator", vibrator_service) - - if vibrator and SDK_INT >= 26: - print("Using android's `VibrationEffect` (SDK >= 26)") - VibrationEffect = autoclass("android.os.VibrationEffect") - vibrator.vibrate( - VibrationEffect.createOneShot( - 1000, VibrationEffect.DEFAULT_AMPLITUDE, - ), - ) - elif vibrator: - print("Using deprecated android's vibrate (SDK < 26)") - vibrator.vibrate(1000) - else: - print('Something happened...vibrator service disabled?') - - def test_ctypes(self, *args): - import ctypes - - def test_numpy(self, *args): - import numpy - - print(numpy.zeros(5)) - print(numpy.arange(5)) - print(numpy.random.random((3, 3))) - - -TestApp().run() diff --git a/testapps/testapp_encryption/colours.png b/testapps/testapp_encryption/colours.png deleted file mode 100644 index 30b685e32b..0000000000 Binary files a/testapps/testapp_encryption/colours.png and /dev/null differ diff --git a/testapps/testapp_encryption/main.py b/testapps/testapp_encryption/main.py deleted file mode 100644 index 3b4db91020..0000000000 --- a/testapps/testapp_encryption/main.py +++ /dev/null @@ -1,360 +0,0 @@ -print('main.py was successfully called') - -import os - -print('imported os') - -print('this dir is', os.path.abspath(os.curdir)) - -print('contents of this dir', os.listdir('./')) - -import sys - -print('pythonpath is', sys.path) - -import kivy - -print('imported kivy') -print('file is', kivy.__file__) - -from kivy.app import App - -from kivy.lang import Builder -from kivy.properties import StringProperty - -from kivy.uix.popup import Popup -from kivy.clock import Clock - -print('Imported kivy') -from kivy.utils import platform - -print('platform is', platform) - -# Test cryptography -try: - from cryptography.fernet import Fernet - - key = Fernet.generate_key() - f = Fernet(key) - cryptography_encrypted = f.encrypt( - b'A really secret message. Not for prying eyes.') - cryptography_decrypted = f.decrypt(cryptography_encrypted) -except Exception as e1: - print('**************************') - print('Error on cryptography operations:\n{}'.format(e1)) - print('**************************') - cryptography_encrypted = 'Error' - cryptography_decrypted = 'Error' - -# Test pycrypto -crypto_hash_message = 'A secret message' -try: - from Crypto.Hash import SHA256 - - hash = SHA256.new() - hash.update(crypto_hash_message) - crypto_hash_hexdigest = hash.hexdigest() -except Exception as e2: - print('**************************') - print('Error on Crypto operations:\n{}'.format(e2)) - print('**************************') - crypto_hash_hexdigest = 'Error' - -# Test scrypt -try: - from scrypt import * - - status_import_scrypt = 'Success' -except ImportError as e3: - print('**************************') - print('Unable to import scrypt:\n{}'.format(e3)) - print('**************************') - status_import_scrypt = 'Error' - -# Test M2Crypto -try: - from M2Crypto import * - - status_import_m2crypto = 'Success' -except ImportError as e5: - print('**************************') - print('Unable to import M2Crypto:\n{}'.format(e5)) - print('**************************\n') - status_import_m2crypto = 'Error' - -# Test pysha3 -try: - import sha3 - - print('Ok imported pysha3, testing some basic operations...') - k = sha3.keccak_512() - k.update(b"data") - print('Test pysha3 operation (keccak_512): {}'.format(k.hexdigest())) - status_import_pysha3 = 'Success' -except ImportError as e6: - print('**************************') - print('Unable to import/operate with pysha3:\n{}'.format(e6)) - print('**************************') - status_import_pysha3 = 'Error' - -# Test pycryptodome -try: - from Crypto.PublicKey import RSA - - print('Ok imported pycryptodome, testing some basic operations...') - secret_code = "Unguessable" - key = RSA.generate(2048) - encrypted_key = key.export_key(passphrase=secret_code, pkcs=8, - protection="scryptAndAES128-CBC") - print('\t -> Testing key for secret code "Unguessable": {}'.format( - encrypted_key)) - - file_out = open("rsa_key.bin", "wb") - file_out.write(encrypted_key) - print('\t -> Testing key write: {}'.format( - 'ok' if os.path.exists(file_out) else 'fail')) - - print('\t -> Testing Public key:'.format(key.publickey().export_key())) - status_import_pycryptodome = 'Success (import and doing simple operations)' -except ImportError as e6: - print('**************************') - print('Unable to import/operate with pycryptodome:\n{}'.format(e6)) - print('**************************') - status_import_pycryptodome = 'Error' - -# Test libtorrent -try: - import libtorrent as lt - - print('Imported libtorrent version {}'.format(lt.version)) - status_import_libtorrent = 'Success (version is: {})'.format(lt.version) -except Exception as e4: - print('**************************') - print('Unable to import libtorrent:\n{}'.format(e4)) - print('**************************') - status_import_libtorrent = 'Error' - -kv = ''' -#:import Metrics kivy.metrics.Metrics -#:import sys sys - -: - size_hint_y: None - height: dp(60) - -: - orientation: 'vertical' - size_hint_y: None - height: self.minimum_height - test_module: '' - test_result: '' - Label: - height: self.texture_size[1] - size_hint_y: None - text_size: self.size[0], None - markup: True - text: '[b]*** TEST {} MODULE ***[/b]'.format(self.parent.test_module) - halign: 'center' - Label: - height: self.texture_size[1] - size_hint_y: None - text_size: self.size[0], None - markup: True - text: - 'Import {}: [color=a0a0a0]{}[/color]'.format( - self.parent.test_module, self.parent.test_result) - halign: 'left' - Widget: - size_hint_y: None - height: 20 - - -ScrollView: - GridLayout: - cols: 1 - size_hint_y: None - height: self.minimum_height - FixedSizeButton: - text: 'test pyjnius' - on_press: app.test_pyjnius() - Label: - height: self.texture_size[1] - size_hint_y: None - text_size: self.size[0], None - markup: True - text: '[b]*** TEST CRYPTOGRAPHY MODULE ***[/b]' - halign: 'center' - Label: - height: self.texture_size[1] - size_hint_y: None - text_size: self.size[0], None - markup: True - text: - 'Cryptography decrypted:\\n[color=a0a0a0]%s[/color]\\n' \\ - 'Cryptography encrypted:\\n[color=a0a0a0]%s[/color]' % ( - app.cryptography_decrypted, app.cryptography_encrypted) - halign: 'left' - Widget: - size_hint_y: None - height: 20 - Label: - height: self.texture_size[1] - size_hint_y: None - text_size: self.size[0], None - markup: True - text: '[b]*** TEST CRYPTO MODULE ***[/b]' - halign: 'center' - Label: - height: self.texture_size[1] - size_hint_y: None - text_size: self.size[0], None - markup: True - text: - 'Crypto message: \\n[color=a0a0a0]%s[/color]\\n'\\ - 'Crypto hex: \\n[color=a0a0a0]%s[/color]' % ( - app.crypto_hash_message, app.crypto_hash_hexdigest) - halign: 'left' - Widget: - size_hint_y: None - height: 20 - TestImport: - test_module: 'scrypt' - test_result: app.status_import_scrypt - TestImport: - test_module: 'm2crypto' - test_result: app.status_import_m2crypto - TestImport: - test_module: 'pysha3' - test_result: app.status_import_pysha3 - TestImport: - test_module: 'pycryptodome' - test_result: app.status_import_pycryptodome - TestImport: - test_module: 'libtorrent' - test_result: app.status_import_libtorrent - Image: - keep_ratio: False - allow_stretch: True - source: 'colours.png' - size_hint_y: None - height: dp(100) - Label: - height: self.texture_size[1] - size_hint_y: None - font_size: 100 - text_size: self.size[0], None - markup: True - text: '[b]Kivy[/b] on [b]SDL2[/b] on [b]Android[/b]!' - halign: 'center' - Label: - height: self.texture_size[1] - size_hint_y: None - text_size: self.size[0], None - markup: True - text: sys.version - halign: 'center' - padding_y: dp(10) - Widget: - size_hint_y: None - height: 20 - Label: - height: self.texture_size[1] - size_hint_y: None - font_size: 50 - text_size: self.size[0], None - markup: True - text: - 'dpi: [color=a0a0a0]%s[/color]\\n'\\ - 'density: [color=a0a0a0]%s[/color]\\n'\\ - 'fontscale: [color=a0a0a0]%s[/color]' % ( - Metrics.dpi, Metrics.density, Metrics.fontscale) - halign: 'center' - FixedSizeButton: - text: 'test ctypes' - on_press: app.test_ctypes() - Widget: - size_hint_y: None - height: 1000 - on_touch_down: print('touched at', args[-1].pos) - -: - title: 'Error' - size_hint: 0.75, 0.75 - Label: - text: root.error_text -''' - - -class ErrorPopup(Popup): - error_text = StringProperty('') - - -def raise_error(error): - print('ERROR:', error) - ErrorPopup(error_text=error).open() - - -class TestApp(App): - cryptography_encrypted = cryptography_encrypted - cryptography_decrypted = cryptography_decrypted - crypto_hash_message = crypto_hash_message - crypto_hash_hexdigest = crypto_hash_hexdigest - status_import_scrypt = status_import_scrypt - status_import_m2crypto = status_import_m2crypto - status_import_pysha3 = status_import_pysha3 - status_import_pycryptodome = status_import_pycryptodome - status_import_libtorrent = status_import_libtorrent - - def build(self): - root = Builder.load_string(kv) - Clock.schedule_interval(self.print_something, 2) - # Clock.schedule_interval(self.test_pyjnius, 5) - print('testing metrics') - from kivy.metrics import Metrics - print('dpi is', Metrics.dpi) - print('density is', Metrics.density) - print('fontscale is', Metrics.fontscale) - return root - - def print_something(self, *args): - print('App print tick', Clock.get_boottime()) - - def on_pause(self): - return True - - def test_pyjnius(self, *args): - try: - from jnius import autoclass, cast - except ImportError: - raise_error('Could not import pyjnius') - return - print('Attempting to vibrate with pyjnius') - ANDROID_VERSION = autoclass('android.os.Build$VERSION') - SDK_INT = ANDROID_VERSION.SDK_INT - - Context = autoclass("android.content.Context") - PythonActivity = autoclass('org.kivy.android.PythonActivity') - activity = PythonActivity.mActivity - - vibrator_service = activity.getSystemService(Context.VIBRATOR_SERVICE) - vibrator = cast("android.os.Vibrator", vibrator_service) - - if vibrator and SDK_INT >= 26: - print("Using android's `VibrationEffect` (SDK >= 26)") - VibrationEffect = autoclass("android.os.VibrationEffect") - vibrator.vibrate( - VibrationEffect.createOneShot( - 1000, VibrationEffect.DEFAULT_AMPLITUDE, - ), - ) - elif vibrator: - print("Using deprecated android's vibrate (SDK < 26)") - vibrator.vibrate(1000) - else: - print('Something happened...vibrator service disabled?') - - def test_ctypes(self, *args): - import ctypes - - -TestApp().run() diff --git a/testapps/testapp_flask/main.py b/testapps/testapp_flask/main.py deleted file mode 100644 index 1f74d1e9e6..0000000000 --- a/testapps/testapp_flask/main.py +++ /dev/null @@ -1,104 +0,0 @@ -print('main.py was successfully called') -print('this is the new main.py') - -import sys -print('python version is: ' + sys.version) -print('python path is', sys.path) - -import os -print('imported os') - -import flask -print('flask1???') - -print('contents of this dir', os.listdir('./')) - -import flask -print('flask???') - - -from flask import Flask -app = Flask(__name__) - -from flask import (Flask, url_for, render_template, request, redirect, - flash) - -print('imported flask etc') -print('importing pyjnius') - -from jnius import autoclass, cast - -ANDROID_VERSION = autoclass('android.os.Build$VERSION') -SDK_INT = ANDROID_VERSION.SDK_INT -Context = autoclass('android.content.Context') -PythonActivity = autoclass('org.kivy.android.PythonActivity') -activity = PythonActivity.mActivity - -vibrator_service = activity.getSystemService(Context.VIBRATOR_SERVICE) -vibrator = cast("android.os.Vibrator", vibrator_service) - -ActivityInfo = autoclass('android.content.pm.ActivityInfo') - -@app.route('/') -def page1(): - return render_template('index.html') - -@app.route('/page2') -def page2(): - return render_template('page2.html') - -@app.route('/vibrate') -def vibrate(): - args = request.args - if 'time' not in args: - print('ERROR: asked to vibrate but without time argument') - print('asked to vibrate', args['time']) - - if vibrator and SDK_INT >= 26: - print("Using android's `VibrationEffect` (SDK >= 26)") - VibrationEffect = autoclass("android.os.VibrationEffect") - vibrator.vibrate( - VibrationEffect.createOneShot( - int(float(args['time']) * 1000), - VibrationEffect.DEFAULT_AMPLITUDE, - ), - ) - elif vibrator: - print("Using deprecated android's vibrate (SDK < 26)") - vibrator.vibrate(int(float(args['time']) * 1000)) - else: - print('Something happened...vibrator service disabled?') - print('vibrated') - -@app.route('/loadUrl') -def loadUrl(): - args = request.args - if 'url' not in args: - print('ERROR: asked to open an url but without url argument') - print('asked to open url', args['url']) - activity.loadUrl(args['url']) - -@app.route('/orientation') -def orientation(): - args = request.args - if 'dir' not in args: - print('ERROR: asked to orient but no dir specified') - direction = args['dir'] - if direction not in ('horizontal', 'vertical'): - print('ERROR: asked to orient to neither horizontal nor vertical') - - if direction == 'horizontal': - activity.setRequestedOrientation( - ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) - else: - activity.setRequestedOrientation( - ActivityInfo.SCREEN_ORIENTATION_PORTRAIT) - - -from os import curdir -from os.path import realpath -print('curdir', realpath(curdir)) -if realpath(curdir).startswith('/data'): - app.run(debug=False) -else: - app.run(debug=True) diff --git a/testapps/testapp_flask/static/colours.png b/testapps/testapp_flask/static/colours.png deleted file mode 100644 index 30b685e32b..0000000000 Binary files a/testapps/testapp_flask/static/colours.png and /dev/null differ diff --git a/testapps/testapp_flask/templates/base.html b/testapps/testapp_flask/templates/base.html deleted file mode 100644 index 63b4881302..0000000000 --- a/testapps/testapp_flask/templates/base.html +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - {% block title %} - Flask on Android - {% endblock %} - - - - - - -

- Flask on Android! -

- - {% block body %} - {% endblock %} - - - - diff --git a/testapps/testapp_flask/templates/index.html b/testapps/testapp_flask/templates/index.html deleted file mode 100644 index 78c38b3eaa..0000000000 --- a/testapps/testapp_flask/templates/index.html +++ /dev/null @@ -1,63 +0,0 @@ -{% extends "base.html" %} - - -{% block body %} - -

Page one

- - - -
- -
- - - - - - - - - - - - - - - - - -{% endblock %} diff --git a/testapps/testapp_flask/templates/page2.html b/testapps/testapp_flask/templates/page2.html deleted file mode 100644 index 70fca15f03..0000000000 --- a/testapps/testapp_flask/templates/page2.html +++ /dev/null @@ -1,15 +0,0 @@ - -{% extends "base.html" %} - - -{% block body %} - -

Page two

- - - -
- -
- -{% endblock %} diff --git a/testapps/testapp_keyboard/colours.png b/testapps/testapp_keyboard/colours.png deleted file mode 100644 index 30b685e32b..0000000000 Binary files a/testapps/testapp_keyboard/colours.png and /dev/null differ diff --git a/testapps/testapp_keyboard/main.py b/testapps/testapp_keyboard/main.py deleted file mode 100644 index 27cea40d18..0000000000 --- a/testapps/testapp_keyboard/main.py +++ /dev/null @@ -1,163 +0,0 @@ -print('main.py was successfully called') - -import os -print('imported os') -import sys -print('imported sys') - -from kivy import platform - -if platform == 'android': - site_dir_path = './_python_bundle/site-packages' - if not os.path.exists(site_dir_path): - print('warning: site-packages dir not found: ' + site_dir_path) - else: - print('contents of ' + site_dir_path) - print(os.listdir(site_dir_path)) - - print('this dir is', os.path.abspath(os.curdir)) - - print('contents of this dir', os.listdir('./')) - - if (os.path.exists(site_dir_path) and - os.path.exists(site_dir_path + '/kivy/app.pyo') - ): - with open(site_dir_path + '/kivy/app.pyo', 'rb') as fileh: - print('app.pyo size is', len(fileh.read())) - -print('pythonpath is', sys.path) - -import kivy -print('imported kivy') -print('file is', kivy.__file__) - -from kivy.app import App - -from kivy.lang import Builder -from kivy.properties import StringProperty - -from kivy.uix.popup import Popup -from kivy.clock import Clock - -print('Imported kivy') -from kivy.utils import platform -print('platform is', platform) - - -kv = ''' -#:import Metrics kivy.metrics.Metrics -#:import Window kivy.core.window.Window - -: - size_hint_y: None - height: dp(60) - - -BoxLayout: - orientation: 'vertical' - BoxLayout: - size_hint_y: None - height: dp(50) - orientation: 'horizontal' - Button: - text: 'None' - on_press: Window.softinput_mode = '' - Button: - text: 'pan' - on_press: Window.softinput_mode = 'pan' - Button: - text: 'below_target' - on_press: Window.softinput_mode = 'below_target' - Button: - text: 'resize' - on_press: Window.softinput_mode = 'resize' - Widget: - Scatter: - id: scatter - size_hint: None, None - size: dp(300), dp(80) - on_parent: self.pos = (300, 100) - BoxLayout: - size: scatter.size - orientation: 'horizontal' - canvas: - Color: - rgba: 1, 0, 0, 1 - Rectangle: - pos: 0, 0 - size: self.size - Widget: - size_hint_x: None - width: dp(30) - TextInput: - text: 'type in me' -''' - - -class ErrorPopup(Popup): - error_text = StringProperty('') - -def raise_error(error): - print('ERROR:', error) - ErrorPopup(error_text=error).open() - -class TestApp(App): - def build(self): - root = Builder.load_string(kv) - Clock.schedule_interval(self.print_something, 2) - # Clock.schedule_interval(self.test_pyjnius, 5) - print('testing metrics') - from kivy.metrics import Metrics - print('dpi is', Metrics.dpi) - print('density is', Metrics.density) - print('fontscale is', Metrics.fontscale) - return root - - def print_something(self, *args): - print('App print tick', Clock.get_boottime()) - - def on_pause(self): - return True - - def test_pyjnius(self, *args): - try: - from jnius import autoclass, cast - except ImportError: - raise_error('Could not import pyjnius') - return - print('Attempting to vibrate with pyjnius') - ANDROID_VERSION = autoclass('android.os.Build$VERSION') - SDK_INT = ANDROID_VERSION.SDK_INT - - Context = autoclass("android.content.Context") - PythonActivity = autoclass('org.kivy.android.PythonActivity') - activity = PythonActivity.mActivity - - vibrator_service = activity.getSystemService(Context.VIBRATOR_SERVICE) - vibrator = cast("android.os.Vibrator", vibrator_service) - - if vibrator and SDK_INT >= 26: - print("Using android's `VibrationEffect` (SDK >= 26)") - VibrationEffect = autoclass("android.os.VibrationEffect") - vibrator.vibrate( - VibrationEffect.createOneShot( - 1000, VibrationEffect.DEFAULT_AMPLITUDE, - ), - ) - elif vibrator: - print("Using deprecated android's vibrate (SDK < 26)") - vibrator.vibrate(1000) - else: - print('Something happened...vibrator service disabled?') - - def test_ctypes(self, *args): - import ctypes - - def test_numpy(self, *args): - import numpy - - print(numpy.zeros(5)) - print(numpy.arange(5)) - print(numpy.random.random((3, 3))) - -TestApp().run() diff --git a/testapps/testapp_matplotlib/main.py b/testapps/testapp_matplotlib/main.py deleted file mode 100644 index 2c2d9d00e7..0000000000 --- a/testapps/testapp_matplotlib/main.py +++ /dev/null @@ -1,75 +0,0 @@ -print('importing numpy') -import numpy as np -print('imported numpy') - -print('importing matplotlib') - -import matplotlib -print('imported matplotlib') - -print('importing pyplot') - -from matplotlib import pyplot as plt - -print('imported pyplot') - -fig, ax = plt.subplots() - -print('created fig and ax') - -ax.plot(np.random.random(50)) - -print('plotted something') - -ax.set_xlabel('test label') - -print('set a label') - -fig.set_size_inches((5, 4)) -fig.savefig('test.png') - -print('saved fig') - -from kivy.app import App -from kivy.base import runTouchApp -from kivy.uix.image import Image -from kivy.lang import Builder - -class MatplotlibApp(App): - def build(self): - root = Builder.load_string(""" -BoxLayout: - orientation: 'vertical' - Image: - id: the_image - source: 'test.png' - allow_stretch: True - Button: - size_hint_y: None - height: dp(40) - text: 'new plot' - on_release: app.generate_new_plot() - """) - return root - - def generate_new_plot(self): - fig, ax = plt.subplots() - ax.set_xlabel('test xlabel') - ax.set_ylabel('test ylabel') - ax.plot(np.random.random(50)) - ax.plot(np.sin(np.linspace(0, 3*np.pi, 30))) - - ax.legend(['random numbers', 'sin']) - - fig.set_size_inches((5, 4)) - fig.tight_layout() - - fig.savefig('test.png', dpi=150) - - self.root.ids.the_image.reload() - - - - -MatplotlibApp().run() -runTouchApp(Image(source='test.png', allow_stretch=True)) diff --git a/testapps/testapp_nogui/main.py b/testapps/testapp_nogui/main.py deleted file mode 100644 index bb7506fb48..0000000000 --- a/testapps/testapp_nogui/main.py +++ /dev/null @@ -1,73 +0,0 @@ - -from math import sqrt -print('import math worked') - -import sys - -print('sys.path is', sys.path) - -for i in range(45, 50): - print(i, sqrt(i)) - -print('trying to import six') -try: - import six -except ImportError: - print('import failed') - - -print('trying to import six again') -try: - import six -except ImportError: - print('import failed (again?)') -print('import six worked!') - -print('Just printing stuff apparently worked, trying pyjnius') - -import jnius - -print('Importing jnius worked') - -print('trying to import stuff') - -try: - from jnius import cast -except ImportError: - print('cast failed') - -try: - from jnius import ensureclass -except ImportError: - print('ensureclass failed') - -try: - from jnius import JavaClass -except ImportError: - print('JavaClass failed') - -try: - from jnius import jnius -except ImportError: - print('jnius failed') - -try: - from jnius import reflect -except ImportError: - print('reflect failed') - -try: - from jnius import find_javaclass -except ImportError: - print('find_javaclass failed') - -print('Trying to autoclass activity') - -from jnius import autoclass - -print('Imported autoclass') - -PythonActivity = autoclass('org.kivy.android.PythonActivity') - -print(':o the autoclass worked!') - diff --git a/testapps/testapp_pillow/colours.png b/testapps/testapp_pillow/colours.png deleted file mode 100644 index 30b685e32b..0000000000 Binary files a/testapps/testapp_pillow/colours.png and /dev/null differ diff --git a/testapps/testapp_pillow/main.py b/testapps/testapp_pillow/main.py deleted file mode 100644 index fa0c1255df..0000000000 --- a/testapps/testapp_pillow/main.py +++ /dev/null @@ -1,416 +0,0 @@ -print("main.py was successfully called") - -import os -import subprocess -from threading import Thread -from functools import partial - -import kivy -from kivy.app import App -from kivy.lang import Builder -from kivy.properties import StringProperty -from kivy.uix.image import Image -from kivy.uix.popup import Popup -from kivy.clock import Clock -from kivy.metrics import dp, sp -from kivy.properties import BooleanProperty, ListProperty - -try: - from PIL import ( - Image as PilImage, - ImageOps, - ImageFont, - ImageDraw, - ImageFilter, - ImageChops, - ) - - status_import_pil = "Succeed" -except Exception as e1: - print("Error on import Pil:\n{}".format(e1)) - status_import_pil = "Errored" - - -def start_func_thread(func, *args, **kwargs): - """Take a function as an argument and start a thread to execute it""" - func_thread = Thread(None, target=func, args=args, kwargs=kwargs) - func_thread.setDaemon(True) - func_thread.start() - - -def add_processed_image(image_path, evt): - """A function that gets our running app and add the processed image we - provide via argument to the ListProperty :attr:`TestApp.processed_images`, - so our app can automatically display the image via kivy's events. - """ - app = App.get_running_app() - app.processed_images.append(image_path) - - -def remove_test_file_if_exist(file_name): - """Check if the provided file (as an argument), exists and remove it in - case that exists.""" - if os.path.isfile(file_name): - print("\t- Removing test file: {}".format(file_name)) - subprocess.call(["rm", file_name]) - - -def test_pil_draw( - text_to_draw="Kivy", - img_target="text_draw.png", - image_width=180, - image_height=100, -): - """A method to test some of the `Pillow`'s package operations: - - - `Image.open` - - `ImageDraw.Draw` - - `ImageFont.truetype` (here we will now if our freetype's recipe - (:class:`~pythonforandroid.recipes.freetype.FreetypeRecipe`) works - as expected - - `ImageFilter.GaussianBlur` - - `Image.composite` - - `ImageChops.invert` - - .. note:: With this test we will know if our freetype library - (:class:`~pythonforandroid.recipes.freetype.FreetypeRecipe`) is - working as expected because we load the default kivy's font (Roboto) - and we use it to draw a text - """ - print( - "Test draw font with pil: {} [width: {}]".format( - text_to_draw, image_width - ) - ) - remove_test_file_if_exist(img_target) - - try: - ttf_font = os.path.join( - os.path.dirname(kivy.__file__), - "data", - "fonts", - "Roboto-Regular.ttf", - ) - img = PilImage.open("colours.png") - img = img.resize((image_width, image_height), PilImage.ANTIALIAS) - font = ImageFont.truetype(ttf_font, int(sp(55))) - - draw = ImageDraw.Draw(img) - for n in range(2, image_width, 2): - draw.rectangle( - (n, n, image_width - n, image_height - n), outline="black" - ) - img.filter(ImageFilter.GaussianBlur(radius=1.5)) - - text_pos = (image_width / 2.0 - int(sp(40)), int(sp(15))) - halo = PilImage.new("RGBA", img.size, (0, 0, 0, 0)) - ImageDraw.Draw(halo).text( - text_pos, text_to_draw, font=font, fill="black" - ) - blurred_halo = halo.filter(ImageFilter.BLUR) - ImageDraw.Draw(blurred_halo).text( - text_pos, text_to_draw, font=font, fill="white" - ) - img = PilImage.composite( - img, blurred_halo, ImageChops.invert(blurred_halo) - ) - - img.save(img_target, "PNG") - except Exception as e: - print("Cannot draw text with pil, error: {}".format(e)) - - if os.path.isfile(img_target): - Clock.schedule_once(partial(add_processed_image, img_target), 0.1) - else: - raise_error("Could not draw text with pil") - - -def test_pil_filter(img_source="text_draw.png", img_target="text_blur.png"): - """A method to test `Pillows`'s `ImageFilter.GaussianBlur`.""" - print("Test pil's gaussian filter: {}".format(img_source)) - remove_test_file_if_exist(img_target) - img = PilImage.open(img_source) - gaussian_image = img.filter(ImageFilter.GaussianBlur(radius=1.5)) - gaussian_image.save(img_target) - if os.path.isfile(img_target): - Clock.schedule_once(partial(add_processed_image, img_target), 0.1) - else: - raise_error("Could not draw text with pil") - - -def test_pil_mirror(img_source="text_draw.png", img_target="text_mirror.png"): - """A method to test `Pillows`'s `ImageOps.mirror`.""" - print("Test convert image to mirror: {}".format(img_source)) - remove_test_file_if_exist(img_target) - - try: - im = PilImage.open(img_source) - mirror_image = ImageOps.mirror(im) - mirror_image.save(img_target) - except Exception as e: - print( - "Cannot make mirrored image for `{}` [ERROR: {}]".format( - img_source, e - ) - ) - - if os.path.isfile(img_target): - Clock.schedule_once(partial(add_processed_image, img_target), 0.1) - else: - raise_error("Could not create a mirrored image") - - -def test_pil_rotate( - img_source="text_draw.png", img_target="text_draw_rotated.png" -): - """A method to test `Pillows`'s `Image.rotate`.""" - print("Test image rotate with image: {}".format(img_source)) - remove_test_file_if_exist(img_target) - - try: - im = PilImage.open(img_source) - im.rotate(180, expand=True).save(img_target, "png") - except Exception as e: - print("Cannot rotate image `{}` [ERROR: {}]".format(img_source, e)) - - if os.path.isfile(img_target): - Clock.schedule_once(partial(add_processed_image, img_target), 0.1) - else: - raise_error("Could not rotate image") - - -kv = """ -#:import Metrics kivy.metrics.Metrics -#:import sys sys - -: - size_hint_y: None - height: dp(100) - -ScrollView: - BoxLayout: - orientation: 'vertical' - size_hint_y: None - height: self.minimum_height - spacing: dp(10) - Image: - keep_ratio: False - allow_stretch: True - source: 'colours.png' - size_hint_y: None - height: dp(100) - Label: - height: self.texture_size[1] - size_hint_y: None - font_size: 100 - text_size: self.size[0], None - markup: True - text: '[b]Kivy[/b] on [b]SDL2[/b] on [b]Android[/b]!' - halign: 'center' - Label: - height: self.texture_size[1] - size_hint_y: None - text_size: self.size[0], None - font_size: 50 - markup: True - text: '[color=a0a0a0]{}[/color]'.format(sys.version) - halign: 'center' - padding_y: dp(10) - Label: - height: self.texture_size[1] - size_hint_y: None - font_size: 50 - text_size: self.size[0], None - markup: True - text: - 'dpi: [color=a0a0a0]{}[/color]\\n'\\ - 'density: [color=a0a0a0]{}[/color]\\n'\\ - 'fontscale: [color=a0a0a0]{}[/color]'.format( - Metrics.dpi, Metrics.density, Metrics.fontscale) - halign: 'center' - Label: - height: self.texture_size[1] - size_hint_y: None - text_size: self.size[0], None - text_color: "0c8916" if "Succeed" in self.text else "bc1607" - font_size: 50 - markup: True - text: - 'Import PIL module: [color={}]{}[/color]'.format( - self.text_color, app.status_import_pil) - halign: 'center' - FixedSizeButton: - text: 'test ctypes' - on_press: app.test_ctypes() - height: dp(60) - FixedSizeButton: - text: 'test pyjnius' - on_press: app.test_pyjnius() - height: dp(60) - BoxLayout: - orientation: 'horizontal' - size_hint_y: None - height: dp(430) - BoxLayout: - orientation: 'vertical' - spacing: dp(10) - FixedSizeButton: - text: 'test Pil draw text' - disabled: - ("Error" in app.status_import_pil or app.processed_draw) - on_release: app.test_pil_draw() - FixedSizeButton: - text: 'test Pil gaussian filter' - disabled: (not app.processed_draw or app.processed_filter) - on_release: app.test_pil_filter() - FixedSizeButton: - text: 'test Pil mirror' - disabled: (not app.processed_filter or app.processed_mirror) - on_release: app.test_pil_mirror() - FixedSizeButton: - text: 'test Pil rotate 180' - disabled: (not app.processed_mirror or app.processed_rotate) - on_release: app.test_pil_rotate() - Widget: - size_hint_x: None - width: dp(10) - BoxLayout: - id: images_container - orientation: 'vertical' - spacing: dp(10) - Widget: - size_hint_y: None - height: dp(430 - 110 * (len(self.parent.children) - 1)) - canvas.before: - Color: - rgba: - (.1, .1, .1, - 1 if len(self.parent.children) < 5 - else 0) - Rectangle: - pos: self.pos[0], self.pos[1] + dp(2) - size: self.size - Widget: - size_hint_y: None - height: dp(10) - -: - title: 'Error' - size_hint: 0.75, 0.75 - Label: - text: root.error_text -""" - - -class ErrorPopup(Popup): - """A pop intended to be used to display error messages""" - error_text = StringProperty("") - - -def raise_error(error): - """Method that will take a message as an argument and will display it in a - a Popup :class:`ErrorPopup`.""" - print("ERROR:", error) - ErrorPopup(error_text=error).open() - - -class TestApp(App): - status_import_pil = status_import_pil - processed_draw = BooleanProperty(False) - processed_filter = BooleanProperty(False) - processed_rotate = BooleanProperty(False) - processed_mirror = BooleanProperty(False) - processed_images = ListProperty() - - def build(self): - return Builder.load_string(kv) - - def on_pause(self): - return True - - def on_stop(self): - print("Removing generated test images...") - for w in self.root.ids.images_container.children[:]: - if hasattr(w, "source"): - self.root.ids.images_container.remove_widget(w) - remove_test_file_if_exist(w.source) - - def test_pyjnius(self, *args): - try: - from jnius import autoclass, cast - except ImportError: - raise_error('Could not import pyjnius') - return - print('Attempting to vibrate with pyjnius') - ANDROID_VERSION = autoclass('android.os.Build$VERSION') - SDK_INT = ANDROID_VERSION.SDK_INT - - Context = autoclass("android.content.Context") - PythonActivity = autoclass('org.kivy.android.PythonActivity') - activity = PythonActivity.mActivity - - vibrator_service = activity.getSystemService(Context.VIBRATOR_SERVICE) - vibrator = cast("android.os.Vibrator", vibrator_service) - - if vibrator and SDK_INT >= 26: - print("Using android's `VibrationEffect` (SDK >= 26)") - VibrationEffect = autoclass("android.os.VibrationEffect") - vibrator.vibrate( - VibrationEffect.createOneShot( - 1000, VibrationEffect.DEFAULT_AMPLITUDE, - ), - ) - elif vibrator: - print("Using deprecated android's vibrate (SDK < 26)") - vibrator.vibrate(1000) - else: - print('Something happened...vibrator service disabled?') - - def test_ctypes(self, *args): - try: - import ctypes - except ImportError: - raise_error("Could not import ctypes") - return - - def test_pil_draw(self, *args): - self.processed_draw = True - start_func_thread( - test_pil_draw, - image_width=int(self.root.width / 2.0), - image_height=int(dp(100)), - ) - - def test_pil_filter(self, *args): - self.processed_filter = True - start_func_thread(test_pil_filter) - - def test_pil_mirror(self, *args): - self.processed_mirror = True - start_func_thread(test_pil_mirror) - - def test_pil_rotate(self, *args): - self.processed_rotate = True - start_func_thread(test_pil_rotate) - - def on_processed_images(self, *args): - print( - "New processed images detected [{} image/s]".format( - len(self.processed_images) - ) - ) - # Display the processed images - while self.processed_images: - img_path = self.processed_images.pop(-1) - print("Building image widget for: {}".format(img_path)) - im = Image( - source=img_path, - size_hint=(1.0, None), - height=dp(100), - keep_ratio=True, - allow_stretch=True, - ) - self.root.ids.images_container.add_widget(im, index=1) - - -TestApp().run() diff --git a/testapps/testapp_service/main.py b/testapps/testapp_service/main.py deleted file mode 100644 index dc781cd3bd..0000000000 --- a/testapps/testapp_service/main.py +++ /dev/null @@ -1,19 +0,0 @@ -print('Service Test App main.py was successfully called') - -import sys -print('python version is: ', sys.version) -print('python sys.path is: ', sys.path) - -from math import sqrt -print('import math worked') - -for i in range(45, 50): - print(i, sqrt(i)) - -print('Just printing stuff apparently worked, trying a simple service') - -from jnius import autoclass -service = autoclass('org.test.testapp_service.ServiceTime') -mActivity = autoclass('org.kivy.android.PythonActivity').mActivity -argument = 'test argument ok' -service.start(mActivity, argument) diff --git a/testapps/testapp_service/p4atime.py b/testapps/testapp_service/p4atime.py deleted file mode 100644 index 75a1d76e0f..0000000000 --- a/testapps/testapp_service/p4atime.py +++ /dev/null @@ -1,19 +0,0 @@ -import datetime -import threading -import time -from os import environ -argument = environ.get('PYTHON_SERVICE_ARGUMENT', '') -print('p4atime.py was successfully called with argument: "{}"'.format(argument)) - -next_call = time.time() - - -def service_timer(): - global next_call - print('P4a datetime service: {}'.format(datetime.datetime.now())) - next_call = next_call + 1 - threading.Timer(next_call - time.time(), service_timer).start() - - -print('Starting the service timer...') -service_timer() diff --git a/testapps/testapp_sqlite_openssl/colours.png b/testapps/testapp_sqlite_openssl/colours.png deleted file mode 100644 index 30b685e32b..0000000000 Binary files a/testapps/testapp_sqlite_openssl/colours.png and /dev/null differ diff --git a/testapps/testapp_sqlite_openssl/main.py b/testapps/testapp_sqlite_openssl/main.py deleted file mode 100644 index 9b75c755ef..0000000000 --- a/testapps/testapp_sqlite_openssl/main.py +++ /dev/null @@ -1,235 +0,0 @@ -print('main.py was successfully called') - -import os -print('imported os') - - -print('this dir is', os.path.abspath(os.curdir)) - -print('contents of this dir', os.listdir('./')) - -import sys -print('pythonpath is', sys.path) - -import kivy -print('imported kivy') -print('file is', kivy.__file__) - -from kivy.app import App - -from kivy.lang import Builder -from kivy.properties import StringProperty - -from kivy.uix.popup import Popup -from kivy.clock import Clock - -print('Imported kivy') -from kivy.utils import platform -print('platform is', platform) - -import peewee -import requests -import sqlite3 - - -try: - inclemnet = requests.get('http://inclem.net/') - print('got inclem.net request') -except: - inclemnet = 'failed inclemnet' - -try: - kivy = requests.get('https://kivy.org/') - print('got kivy request (https)') -except: - kivy = 'failed kivy' - -from peewee import * -db = SqliteDatabase('test.db') - -class Person(Model): - name = CharField() - birthday = DateField() - is_relative = BooleanField() - - class Meta: - database = db - - def __repr__(self): - return ''.format(self.name, self.birthday) - - def __str__(self): - return repr(self) - -db.connect() -try: - db.create_tables([Person]) -except: - import traceback - traceback.print_exc() - -import random -from datetime import date -test_person = Person(name='person{}'.format(random.randint(0, 1000)), - birthday=date(random.randint(1900, 2000), random.randint(1, 9), random.randint(1, 20)), - is_relative=False) -test_person.save() - - -kv = ''' -#:import Metrics kivy.metrics.Metrics -#:import sys sys - -: - size_hint_y: None - height: dp(60) - - -ScrollView: - GridLayout: - cols: 1 - size_hint_y: None - height: self.minimum_height - FixedSizeButton: - text: 'test pyjnius' - on_press: app.test_pyjnius() - Label: - height: self.texture_size[1] - size_hint_y: None - text_size: self.size[0], None - markup: True - text: 'kivy request: {}\\ninclemnet request: {}'.format(app.kivy_request, app.inclemnet_request) - halign: 'center' - Label: - height: self.texture_size[1] - size_hint_y: None - text_size: self.size[0], None - markup: True - text: 'people: {}'.format(app.people) - halign: 'center' - Image: - keep_ratio: False - allow_stretch: True - source: 'colours.png' - size_hint_y: None - height: dp(100) - Label: - height: self.texture_size[1] - size_hint_y: None - font_size: 100 - text_size: self.size[0], None - markup: True - text: '[b]Kivy[/b] on [b]SDL2[/b] on [b]Android[/b]!' - halign: 'center' - Label: - height: self.texture_size[1] - size_hint_y: None - text_size: self.size[0], None - markup: True - text: sys.version - halign: 'center' - padding_y: dp(10) - Widget: - size_hint_y: None - height: 20 - Label: - height: self.texture_size[1] - size_hint_y: None - font_size: 50 - text_size: self.size[0], None - markup: True - text: 'dpi: {}\\ndensity: {}\\nfontscale: {}'.format(Metrics.dpi, Metrics.density, Metrics.fontscale) - halign: 'center' - FixedSizeButton: - text: 'test ctypes' - on_press: app.test_ctypes() - FixedSizeButton: - text: 'test numpy' - on_press: app.test_numpy() - Widget: - size_hint_y: None - height: 1000 - on_touch_down: print('touched at', args[-1].pos) - -: - title: 'Error' - size_hint: 0.75, 0.75 - Label: - text: root.error_text -''' - - -class ErrorPopup(Popup): - error_text = StringProperty('') - -def raise_error(error): - print('ERROR:', error) - ErrorPopup(error_text=error).open() - -class TestApp(App): - - kivy_request = kivy - inclemnet_request = inclemnet - - people = ', '.join(map(str, list(Person.select()))) - - def build(self): - root = Builder.load_string(kv) - Clock.schedule_interval(self.print_something, 2) - # Clock.schedule_interval(self.test_pyjnius, 5) - print('testing metrics') - from kivy.metrics import Metrics - print('dpi is', Metrics.dpi) - print('density is', Metrics.density) - print('fontscale is', Metrics.fontscale) - return root - - def print_something(self, *args): - print('App print tick', Clock.get_boottime()) - - def on_pause(self): - return True - - def test_pyjnius(self, *args): - try: - from jnius import autoclass, cast - except ImportError: - raise_error('Could not import pyjnius') - return - print('Attempting to vibrate with pyjnius') - ANDROID_VERSION = autoclass('android.os.Build$VERSION') - SDK_INT = ANDROID_VERSION.SDK_INT - - Context = autoclass("android.content.Context") - PythonActivity = autoclass('org.kivy.android.PythonActivity') - activity = PythonActivity.mActivity - - vibrator_service = activity.getSystemService(Context.VIBRATOR_SERVICE) - vibrator = cast("android.os.Vibrator", vibrator_service) - - if vibrator and SDK_INT >= 26: - print("Using android's `VibrationEffect` (SDK >= 26)") - VibrationEffect = autoclass("android.os.VibrationEffect") - vibrator.vibrate( - VibrationEffect.createOneShot( - 1000, VibrationEffect.DEFAULT_AMPLITUDE, - ), - ) - elif vibrator: - print("Using deprecated android's vibrate (SDK < 26)") - vibrator.vibrate(1000) - else: - print('Something happened...vibrator service disabled?') - - def test_ctypes(self, *args): - pass - - def test_numpy(self, *args): - import numpy - - print(numpy.zeros(5)) - print(numpy.arange(5)) - print(numpy.random.random((3, 3))) - - -TestApp().run() diff --git a/tests/recipes/test_libbz2.py b/tests/recipes/test_libbz2.py new file mode 100644 index 0000000000..72d26498d8 --- /dev/null +++ b/tests/recipes/test_libbz2.py @@ -0,0 +1,25 @@ +import unittest +from tests.recipes.recipe_lib_test import BaseTestForMakeRecipe + + +class TestLibBz2Recipe(BaseTestForMakeRecipe, unittest.TestCase): + """ + An unittest for recipe :mod:`~pythonforandroid.recipes.libbz2` + """ + recipe_name = "libbz2" + sh_command_calls = [] + + def test_get_library_includes(self): + self.assertEqual( + self.recipe.get_library_includes(self.arch), + f" -I{self.recipe.get_build_dir(self.arch.arch)}", + ) + + def test_get_library_ldflags(self): + self.assertEqual( + self.recipe.get_library_ldflags(self.arch), + f" -L{self.recipe.get_build_dir(self.arch.arch)}", + ) + + def test_link_libs_flags(self): + self.assertEqual(self.recipe.get_library_libs_flag(), " -lbz2") diff --git a/tests/recipes/test_liblzma.py b/tests/recipes/test_liblzma.py new file mode 100644 index 0000000000..7cb76d1588 --- /dev/null +++ b/tests/recipes/test_liblzma.py @@ -0,0 +1,26 @@ +import unittest +from os.path import join +from tests.recipes.recipe_lib_test import BaseTestForMakeRecipe + + +class TestLibLzmaRecipe(BaseTestForMakeRecipe, unittest.TestCase): + """ + An unittest for recipe :mod:`~pythonforandroid.recipes.liblzma` + """ + recipe_name = "liblzma" + sh_command_calls = ["./autogen.sh", "autoreconf", "./configure"] + + def test_get_library_includes(self): + self.assertEqual( + self.recipe.get_library_includes(self.arch), + f" -I{join(self.recipe.get_build_dir(self.arch.arch), 'install/include')}", + ) + + def test_get_library_ldflags(self): + self.assertEqual( + self.recipe.get_library_ldflags(self.arch), + f" -L{join(self.recipe.get_build_dir(self.arch.arch), 'install/lib')}", + ) + + def test_link_libs_flags(self): + self.assertEqual(self.recipe.get_library_libs_flag(), " -llzma") diff --git a/tests/recipes/test_pandas.py b/tests/recipes/test_pandas.py new file mode 100644 index 0000000000..df91c2398b --- /dev/null +++ b/tests/recipes/test_pandas.py @@ -0,0 +1,49 @@ +import unittest +from os.path import join +from unittest import mock +from tests.recipes.recipe_lib_test import RecipeCtx + + +class TestPandasRecipe(RecipeCtx, unittest.TestCase): + """ + An unittest for recipe :mod:`~pythonforandroid.recipes.pandas` + """ + recipe_name = "pandas" + + @mock.patch("pythonforandroid.recipe.Recipe.check_recipe_choices") + @mock.patch("pythonforandroid.build.ensure_dir") + @mock.patch("pythonforandroid.archs.glob") + @mock.patch("pythonforandroid.archs.find_executable") + def test_get_recipe_env( + self, + mock_find_executable, + mock_glob, + mock_ensure_dir, + mock_check_recipe_choices, + ): + """ + Test that method + :meth:`~pythonforandroid.recipes.pandas.PandasRecipe.get_recipe_env` + returns the expected flags + """ + + mock_find_executable.return_value = ( + "/opt/android/android-ndk/toolchains/" + "llvm/prebuilt/linux-x86_64/bin/clang" + ) + mock_glob.return_value = ["llvm"] + mock_check_recipe_choices.return_value = sorted( + self.ctx.recipe_build_order + ) + numpy_includes = join( + self.ctx.get_python_install_dir(), "numpy/core/include", + ) + env = self.recipe.get_recipe_env(self.arch) + self.assertIn(numpy_includes, env["NUMPY_INCLUDES"]) + self.assertIn(" -landroid", env["LDFLAGS"]) + + # make sure that the mocked methods are actually called + mock_glob.assert_called() + mock_ensure_dir.assert_called() + mock_find_executable.assert_called() + mock_check_recipe_choices.assert_called()