diff --git a/.dockerignore b/.dockerignore index 9a59c66..55bd4f0 100644 --- a/.dockerignore +++ b/.dockerignore @@ -3,3 +3,4 @@ venv/ .buildozer/ .pytest_cache/ .tox/ +htmlcov/ diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml new file mode 100644 index 0000000..4f18963 --- /dev/null +++ b/.github/workflows/android.yml @@ -0,0 +1,13 @@ +name: Android +on: [push, pull_request] + +jobs: + build: + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v2 + - run: docker pull kivy/buildozer + - run: make docker/run/buildozer + - uses: actions/upload-artifact@v2 + with: + path: bin/xcamera-*-armeabi-v7a-debug.apk diff --git a/.github/workflows/pypi-release.yml b/.github/workflows/pypi-release.yml new file mode 100644 index 0000000..c67a876 --- /dev/null +++ b/.github/workflows/pypi-release.yml @@ -0,0 +1,24 @@ +name: PyPI release +on: [push] + +jobs: + pypi: + runs-on: ubuntu-20.04 + strategy: + matrix: + config: + - {setup_file: setup.py, secret_key: pypi_password_xcamera} + - {setup_file: setup_meta.py, secret_key: pypi_password_kivy_garden_xcamera} + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + with: + python-version: 3.7 + - run: python -m pip install --user --upgrade setuptools wheel kivy + - run: python setup.py sdist bdist_wheel + - name: Publish package + if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') + uses: pypa/gh-action-pypi-publish@v1.2.2 + with: + user: __token__ + password: ${{ secrets[matrix.secret_key] }} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..fe895ad --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,16 @@ +name: Tests +on: [push, pull_request] + +jobs: + tests: + runs-on: ubuntu-20.04 + env: + DISPLAY: ':99.0' + steps: + - uses: actions/checkout@v2 + - run: make docker/pull + - run: make docker/build + - run: | + sudo apt install xvfb libxkbcommon-x11-0 + Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 & + - run: make docker/run/test diff --git a/.travis.yml b/.travis.yml index 2b9d4c6..2e0f490 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,19 +4,11 @@ language: generic services: - docker - -env: - - DISPLAY=:99.0 - -before_install: - - sudo apt update -qq > /dev/null - - sudo apt install --yes --no-install-recommends xvfb - -install: - - make docker/build + - xvfb before_script: - - sh -e /etc/init.d/xvfb start + - make docker/pull + - make docker/build script: - make docker/run/test diff --git a/CHANGELOG.md b/CHANGELOG.md index 880838a..745b358 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Change Log +## [2020.0613] + + - Increase test coverage to 93% + - Move PythonActivity from 'kivy' after 'renpy' drop, closes #15, refs #16 + - Introduces GitHub actions, refs #18 + - Also tests against Python 3.8, refs #19 + - Android build CI, refs #20 + - Uploads APK artifact, refs #21 + - Automates PyPI releases from CI, refs #22 + - Bumps to kivy post 2.0.0rc2 on Android, refs #23 + ## [2019.0928] - Setup coverage testing diff --git a/Makefile b/Makefile index 54a8883..65f1322 100644 --- a/Makefile +++ b/Makefile @@ -11,43 +11,39 @@ SOURCES=src/ tests/ setup.py setup_meta.py SPHINXBUILD=$(shell realpath venv/bin/sphinx-build) DOCS_DIR=doc SYSTEM_DEPENDENCIES= \ - build-essential \ - ccache \ - cmake \ - curl \ git \ libsdl2-dev \ libsdl2-image-dev \ libsdl2-mixer-dev \ libsdl2-ttf-dev \ - libpython3.6-dev \ - libpython$(PYTHON_VERSION)-dev \ - libzbar-dev \ pkg-config \ - python3.6 \ - python3.6-dev \ python3.7 \ - python3.7-dev \ + python$(PYTHON_VERSION) \ tox \ virtualenv -OS=$(shell lsb_release -si) PYTHON_MAJOR_VERSION=3 -PYTHON_MINOR_VERSION=7 +PYTHON_MINOR_VERSION=8 PYTHON_VERSION=$(PYTHON_MAJOR_VERSION).$(PYTHON_MINOR_VERSION) PYTHON_MAJOR_MINOR=$(PYTHON_MAJOR_VERSION)$(PYTHON_MINOR_VERSION) PYTHON_WITH_VERSION=python$(PYTHON_VERSION) +DOCKER_IMAGE_LINUX=kivy/xcamera-linux +DOCKER_ENV=--env-file dockerfiles/env.list +DOCKER_VOLUME=--volume /tmp/.X11-unix:/tmp/.X11-unix + +ifndef CI +DOCKER_DEVICE=--device /dev/video0 +DOCKER_GROUP_ADD=--group-add video +endif -all: system_dependencies virtualenv +all: virtualenv system_dependencies: -ifeq ($(OS), Ubuntu) - sudo apt install --yes --no-install-recommends $(SYSTEM_DEPENDENCIES) -endif + apt install --yes --no-install-recommends $(SYSTEM_DEPENDENCIES) $(VIRTUAL_ENV): - virtualenv -p $(PYTHON_WITH_VERSION) $(VIRTUAL_ENV) - $(PIP) install Cython==0.28.6 + virtualenv --python $(PYTHON_WITH_VERSION) $(VIRTUAL_ENV) + $(PIP) install Cython $(PIP) install -r requirements.txt virtualenv: $(VIRTUAL_ENV) @@ -60,7 +56,6 @@ run: virtualenv test: $(TOX) - @if test -n "$$CI"; then .tox/py$(PYTHON_MAJOR_MINOR)/bin/coveralls; fi; \ pytest: virtualenv/test PYTHONPATH=src $(PYTEST) --cov src/ --cov-report html tests/ @@ -94,7 +89,7 @@ release/upload: $(TWINE) upload dist/* clean: release/clean docs/clean - py3clean src/ + py3clean . find . -type d -name "__pycache__" -exec rm -r {} + find . -type d -name "*.egg-info" -exec rm -r {} + rm -rf htmlcov/ @@ -102,14 +97,22 @@ clean: release/clean docs/clean clean/all: clean rm -rf $(VIRTUAL_ENV) .tox/ +docker/pull: + docker pull $(DOCKER_IMAGE_LINUX):latest + docker/build: - docker build --tag=xcamera-linux --file=dockerfiles/Dockerfile-linux . + docker build --cache-from=$(DOCKER_IMAGE_LINUX) --tag=$(DOCKER_IMAGE_LINUX) --file=dockerfiles/Dockerfile-linux . docker/run/test: - docker run --env-file dockerfiles/env.list -v /tmp/.X11-unix:/tmp/.X11-unix xcamera-linux 'make test' + docker run --rm $(DOCKER_ENV) $(DOCKER_VOLUME) $(DOCKER_GROUP_ADD) $(DOCKER_DEVICE) $(DOCKER_IMAGE_LINUX) 'make test' docker/run/app: - docker run --env-file dockerfiles/env.list -v /tmp/.X11-unix:/tmp/.X11-unix --device=/dev/video0:/dev/video0 xcamera-linux 'make run' + docker run --rm $(DOCKER_ENV) $(DOCKER_VOLUME) $(DOCKER_GROUP_ADD) $(DOCKER_DEVICE) $(DOCKER_IMAGE_LINUX) 'make run' docker/run/shell: - docker run --env-file dockerfiles/env.list -v /tmp/.X11-unix:/tmp/.X11-unix --device=/dev/video0:/dev/video0 -it --rm xcamera-linux + docker run -it --rm $(DOCKER_ENV) $(DOCKER_VOLUME) $(DOCKER_GROUP_ADD) $(DOCKER_DEVICE) $(DOCKER_IMAGE_LINUX) + +docker/run/buildozer: + mkdir -p ~/.buildozer .buildozer bin + @if test -n "$$CI"; then sudo chown -R 1000:1000 ~/.buildozer .buildozer bin; fi; \ + docker run --volume "$(CURDIR)":/home/user/hostcwd kivy/buildozer android debug diff --git a/README.md b/README.md index 635e1cf..0565eca 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # XCamera: Android-optimized camera widget +[![Github Actions Tests](https://github.com/kivy-garden/xcamera/workflows/Tests/badge.svg)](https://github.com/kivy-garden/xcamera/actions?query=workflow%3ATests) +[![Github Actions Android](https://github.com/kivy-garden/xcamera/workflows/Android/badge.svg)](https://github.com/kivy-garden/xcamera/actions?query=workflow%3AAndroid) [![Build Status](https://travis-ci.com/kivy-garden/xcamera.svg?branch=develop)](https://travis-ci.com/kivy-garden/xcamera) [![Coverage Status](https://coveralls.io/repos/github/kivy-garden/xcamera/badge.svg?branch=develop)](https://coveralls.io/github/kivy-garden/xcamera?branch=develop) [![PyPI version](https://badge.fury.io/py/xcamera.svg)](https://badge.fury.io/py/xcamera) @@ -40,7 +42,7 @@ Notes: [xcamera is available on PyPI](https://pypi.org/project/xcamera/). Therefore it can be installed via `pip`. ```sh -pip3 install --user xcamera +pip3 install xcamera ``` Once installed, the demo should be available in your `PATH` and can be ran from the command line. ```sh diff --git a/buildozer.spec b/buildozer.spec index f58f3c8..4a45f9a 100644 --- a/buildozer.spec +++ b/buildozer.spec @@ -38,7 +38,7 @@ version.filename = %(source.dir)s/kivy_garden/xcamera/version.py # comma separated e.g. requirements = sqlite3,kivy requirements = android, - kivy==1.11.1, + kivy==00034a5, python3 # (str) Custom source folders for requirements diff --git a/dockerfiles/Dockerfile-linux b/dockerfiles/Dockerfile-linux index 886d202..7bad390 100644 --- a/dockerfiles/Dockerfile-linux +++ b/dockerfiles/Dockerfile-linux @@ -1,15 +1,15 @@ # Docker image for installing dependencies on Linux and running tests. # Build with: -# docker build --tag=xcamera-linux --file=dockerfiles/Dockerfile-linux . +# docker build --tag=kivy/xcamera-linux --file=dockerfiles/Dockerfile-linux . # Run with: -# docker run xcamera-linux /bin/sh -c 'make test' +# docker run kivy/xcamera-linux /bin/sh -c 'make test' # Or using the entry point shortcut: -# docker run xcamera-linux 'make test' +# docker run kivy/xcamera-linux 'make test' # For running UI: # xhost +"local:docker@" -# docker run -e DISPLAY -v /tmp/.X11-unix:/tmp/.X11-unix --device=/dev/video0:/dev/video0 xcamera-linux 'make run' +# docker run -e DISPLAY -v /tmp/.X11-unix:/tmp/.X11-unix --device=/dev/video0:/dev/video0 kivy/xcamera-linux 'make run' # Or for interactive shell: -# docker run -it --rm xcamera-linux +# docker run -it --rm kivy/xcamera-linux FROM ubuntu:18.04 ENV USER="user" @@ -25,8 +25,7 @@ ENV LANG="en_US.UTF-8" \ LC_ALL="en_US.UTF-8" # install system dependencies -RUN apt install --yes --no-install-recommends \ - lsb-release \ +RUN apt update -qq > /dev/null && apt install --yes --no-install-recommends \ make \ sudo @@ -40,6 +39,8 @@ RUN gpasswd --add ${USER} video USER ${USER} WORKDIR ${WORK_DIR} +COPY Makefile requirements* ${WORK_DIR}/ +RUN sudo apt update -qq > /dev/null \ + && sudo make system_dependencies && make virtualenv COPY . ${WORK_DIR} -RUN sudo make system_dependencies && make virtualenv ENTRYPOINT ["./dockerfiles/start.sh"] diff --git a/requirements.txt b/requirements.txt index e893041..b47b710 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ -Kivy==1.11.1 -opencv-python==4.1.1.26 +Kivy==2.0.0rc2 +opencv-python==4.1.2.30 diff --git a/src/kivy_garden/xcamera/android_api.py b/src/kivy_garden/xcamera/android_api.py index 851ab39..1beb5b8 100644 --- a/src/kivy_garden/xcamera/android_api.py +++ b/src/kivy_garden/xcamera/android_api.py @@ -4,7 +4,7 @@ Camera = autoclass('android.hardware.Camera') AndroidActivityInfo = autoclass('android.content.pm.ActivityInfo') -AndroidPythonActivity = autoclass('org.renpy.android.PythonActivity') +AndroidPythonActivity = autoclass('org.kivy.android.PythonActivity') PORTRAIT = AndroidActivityInfo.SCREEN_ORIENTATION_PORTRAIT LANDSCAPE = AndroidActivityInfo.SCREEN_ORIENTATION_LANDSCAPE diff --git a/src/kivy_garden/xcamera/version.py b/src/kivy_garden/xcamera/version.py index c33baeb..51ac59e 100644 --- a/src/kivy_garden/xcamera/version.py +++ b/src/kivy_garden/xcamera/version.py @@ -1 +1 @@ -__version__ = '2019.0928' +__version__ = '2020.0613' diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/kivy_garden/__init__.py b/tests/kivy_garden/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/kivy_garden/xcamera/__init__.py b/tests/kivy_garden/xcamera/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/kivy_garden/xcamera/test_main.py b/tests/kivy_garden/xcamera/test_main.py new file mode 100644 index 0000000..d2f5f2b --- /dev/null +++ b/tests/kivy_garden/xcamera/test_main.py @@ -0,0 +1,40 @@ +from threading import Thread +from time import sleep +from unittest import mock + +from kivy.app import App +from kivy.clock import Clock + +from kivy_garden.xcamera.main import CameraApp, main +from tests.test_main import camera_release_workaround, patch_core_camera + + +def patch_picture_taken(): + return mock.patch('kivy_garden.xcamera.main.CameraApp.picture_taken') + + +class TestMain: + """ + Tests the `main` module. + """ + + def test_picture_taken(self): + """ + Checks the `picture_taken()` listener gets called on the running app. + """ + app_thread = Thread(target=main) + app_thread.start() + app = App.get_running_app() + filename = mock.sentinel + Clock.schedule_once( + lambda dt: app.root.ids.xcamera.dispatch( + 'on_picture_taken', filename)) + with patch_picture_taken() as m_picture_taken, patch_core_camera(): + sleep(0.5) # FIXME: nondeterministic approach + # makes sure app thread is gracefully stopped before asserting + app.stop() + app_thread.join() + camera_release_workaround(app) + assert type(app) == CameraApp + assert m_picture_taken.mock_calls == [ + mock.call(app.root.ids.xcamera, filename)] diff --git a/tests/test_main.py b/tests/test_main.py index f27fcd9..f0a8691 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,14 +1,52 @@ +import os +from threading import Thread from unittest import mock +from kivy.app import App + +from kivy_garden.xcamera.main import CameraApp from main import main +def camera_release_workaround(app): + """ + Upstream bug workaround, refs: + https://github.com/kivy-garden/xcamera/issues/14 + """ + app.root.ids.xcamera._camera._device.release() + + +def get_camera_class(): + """ + Continuous integration providers don't have a camera available. + """ + if os.environ.get('CI', False): + Camera = None + else: + from kivy.core.camera import Camera + return Camera + + +def patch_core_camera(): + Camera = get_camera_class() + return mock.patch('kivy.uix.camera.CoreCamera', wraps=Camera) + + class TestMain: """ Tests the `main` module. """ def test_main(self): - with mock.patch('kivy_garden.xcamera.main.CameraApp.run') as m_play: - main() - assert m_play.mock_calls == [mock.call()] + """ + Checks the main starts the app properly. + """ + app_thread = Thread(target=main) + app_thread.start() + app = App.get_running_app() + # makes sure app thread is gracefully stopped before asserting + app.stop() + with patch_core_camera(): + app_thread.join() + camera_release_workaround(app) + assert type(app) == CameraApp diff --git a/tox.ini b/tox.ini index ffc84d2..2929cce 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = pep8,isort-check,py36,py37 +envlist = pep8,isort-check,py36,py37,py38 # no setup.py to be ran skipsdist = True @@ -11,10 +11,9 @@ passenv = CI DISPLAY deps = - -r{toxinidir}/requirements.txt - -r{toxinidir}/requirements/requirements-test.txt -commands = - pytest --cov src/ tests/ + -r{toxinidir}/requirements.txt + -r{toxinidir}/requirements/requirements-test.txt +commands = pytest -s --cov src/ tests/ [testenv:pep8] commands = flake8 {env:SOURCES}