From 035bbf7fff80b3d6e7bd412bd26d3fc7eeaf1c3d Mon Sep 17 00:00:00 2001 From: Russell Martin Date: Tue, 21 Nov 2023 13:57:33 -0500 Subject: [PATCH] Introduce package for Briefcase Automation --- .github/workflows/ci.yml | 15 +++- automation/README.md | 12 ++++ automation/pyproject.toml | 28 ++++++++ automation/src/__init__.py | 0 automation/src/automation/__init__.py | 0 .../src/automation/bootstraps/__init__.py | 0 .../automation/bootstraps/pursuedpybear.py | 69 +++++++++++++++++++ .../src/automation/bootstraps/pygame.py | 42 +++++++++++ .../src/automation/bootstraps/pyside6.py | 40 +++++++++++ automation/src/automation/bootstraps/toga.py | 31 +++++++++ changes/1549.misc.rst | 1 + pyproject.toml | 1 + tox.ini | 5 +- 13 files changed, 239 insertions(+), 5 deletions(-) create mode 100644 automation/README.md create mode 100644 automation/pyproject.toml create mode 100644 automation/src/__init__.py create mode 100644 automation/src/automation/__init__.py create mode 100644 automation/src/automation/bootstraps/__init__.py create mode 100644 automation/src/automation/bootstraps/pursuedpybear.py create mode 100644 automation/src/automation/bootstraps/pygame.py create mode 100644 automation/src/automation/bootstraps/pyside6.py create mode 100644 automation/src/automation/bootstraps/toga.py create mode 100644 changes/1549.misc.rst diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 27078086d..f89a09bcc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,7 +33,10 @@ jobs: package: name: Python package - uses: beeware/.github/.github/workflows/python-package-create.yml@main +# uses: beeware/.github/.github/workflows/python-package-create.yml@main + uses: rmartin16/.github-beeware/.github/workflows/python-package-create.yml@build-verify-run # TODO:PR: remove me + with: + tox-factors: -with-automation unit-tests: name: Unit tests @@ -145,10 +148,13 @@ jobs: verify-projects: name: Verify project needs: unit-tests - uses: beeware/.github/.github/workflows/app-create-verify.yml@main +# uses: beeware/.github/.github/workflows/app-create-verify.yml@main + uses: rmartin16/.github-beeware/.github/workflows/app-create-verify.yml@build-verify-run # TODO:PR: remove me with: runner-os: ${{ matrix.runner-os }} framework: ${{ matrix.framework }} + workflow-repo: rmartin16/.github-beeware # TODO:PR: REMOVE ME + workflow-repo-ref: build-verify-run # TODO:PR: REMOVE ME strategy: fail-fast: false matrix: @@ -158,7 +164,8 @@ jobs: verify-apps: name: Build app needs: unit-tests - uses: beeware/.github/.github/workflows/app-build-verify.yml@main +# uses: beeware/.github/.github/workflows/app-build-verify.yml@main + uses: rmartin16/.github-beeware/.github/workflows/app-build-verify.yml@build-verify-run # TODO:PR: remove me with: # This *must* be the version of Python that is the system Python on the # Ubuntu version used to run Linux tests. We use a fixed ubuntu-22.04 @@ -168,6 +175,8 @@ jobs: python-version: "3.10" runner-os: ${{ matrix.runner-os }} framework: ${{ matrix.framework }} + workflow-repo: rmartin16/.github-beeware # TODO:PR: REMOVE ME + workflow-repo-ref: build-verify-run # TODO:PR: REMOVE ME strategy: fail-fast: false matrix: diff --git a/automation/README.md b/automation/README.md new file mode 100644 index 000000000..3b93f0e2d --- /dev/null +++ b/automation/README.md @@ -0,0 +1,12 @@ +## Briefcase Automation + +This package provides Briefcase plugins to facilitate automation in CI. + +This package is internal to Briefcase's own development and is not needed to create, +develop, or distribute apps created with Briefcase. + +### Bootstraps + +There are bootstrap plugins for each GUI toolkit. The bootstrap allows for Briefcase +to create a project using the toolkit but when the project's app run, the app +automatically exits after a few seconds. diff --git a/automation/pyproject.toml b/automation/pyproject.toml new file mode 100644 index 000000000..7922c86a9 --- /dev/null +++ b/automation/pyproject.toml @@ -0,0 +1,28 @@ +[build-system] +requires = [ + # keep versions in sync with ../pyproject.toml + "setuptools==69.0.0", + "setuptools_scm==8.0.4", + "setuptools_dynamic_dependencies @ git+https://github.com/beeware/setuptools_dynamic_dependencies", +] +build-backend = "setuptools.build_meta" + +[project] +name = "x-briefcase-automation" +description = "A Briefcase plugin for CI automation." +readme = "README.md" +license.text = "New BSD" +classifiers = ["Private :: Do Not Upload"] +dynamic = ["version", "dependencies"] + +[project.entry-points."briefcase.bootstraps"] +"Toga Automation" = "automation.bootstraps.toga:TogaAutomationBootstrap" +"PySide6 Automation" = "automation.bootstraps.pyside6:PySide6AutomationBootstrap" +"Pygame Automation" = "automation.bootstraps.pygame:PygameAutomationBootstrap" +"PursuedPyBear Automation" = "automation.bootstraps.pursuedpybear:PursuedPyBearAutomationBootstrap" + +[tool.setuptools_scm] +root = "../" + +[tool.setuptools_dynamic_dependencies] +dependencies = ["briefcase == {version}"] diff --git a/automation/src/__init__.py b/automation/src/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/automation/src/automation/__init__.py b/automation/src/automation/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/automation/src/automation/bootstraps/__init__.py b/automation/src/automation/bootstraps/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/automation/src/automation/bootstraps/pursuedpybear.py b/automation/src/automation/bootstraps/pursuedpybear.py new file mode 100644 index 000000000..cab9e9b3e --- /dev/null +++ b/automation/src/automation/bootstraps/pursuedpybear.py @@ -0,0 +1,69 @@ +import tomli_w + +try: + import tomllib +except ModuleNotFoundError: + import tomli as tomllib + +from briefcase.bootstraps import PursuedPyBearGuiBootstrap + + +class PursuedPyBearAutomationBootstrap(PursuedPyBearGuiBootstrap): + def app_source(self): + return """\ +import importlib.metadata +import os +import sys + +import ppb + + +class {{ cookiecutter.class_name }}(ppb.Scene): + def __init__(self, **props): + super().__init__(**props) + self.updates: int = 0 + + self.add( + ppb.Sprite( + image=ppb.Image("{{ cookiecutter.module_name }}/resources/{{ cookiecutter.app_name }}.png"), + ) + ) + + def on_update(self, event, signal): + self.updates += 1 + # quit after 2 seconds since on_update is run 60 times/second + if self.updates > 120: + print(">>> successfully started...exiting <<<") + print(">>>>>>>>>> EXIT 0 <<<<<<<<<<") + signal(ppb.events.Quit()) + + +def main(): + app_module = sys.modules["__main__"].__package__ + metadata = importlib.metadata.metadata(app_module) + + os.environ["SDL_VIDEO_X11_WMCLASS"] = metadata["Formal-Name"] + + ppb.run( + starting_scene={{ cookiecutter.class_name }}, + title=metadata["Formal-Name"], + ) +""" + + # The constraint of pysdl2-dll==2.0.22 is required for ppb==1.1.0; + # the libraries in later versions of pysdl2-dll are not compatible. + + def pyproject_table_linux_flatpak(self): + table = tomllib.loads(super().pyproject_table_linux_flatpak()) + table.setdefault("requires", []).append("pysdl2-dll==2.0.22") + return f"\n{tomli_w.dumps(table)}" + + def pyproject_table_windows(self): + table = tomllib.loads(super().pyproject_table_windows()) + table.setdefault("requires", []).append("pysdl2-dll==2.0.22") + return f"\n{tomli_w.dumps(table)}" + + def pyproject_table_macOS(self): + table = tomllib.loads(super().pyproject_table_macOS()) + table.setdefault("requires", []).append("pysdl2-dll==2.0.22") + return f"\n{tomli_w.dumps(table)}" diff --git a/automation/src/automation/bootstraps/pygame.py b/automation/src/automation/bootstraps/pygame.py new file mode 100644 index 000000000..1bac648fc --- /dev/null +++ b/automation/src/automation/bootstraps/pygame.py @@ -0,0 +1,42 @@ +from briefcase.bootstraps import PygameGuiBootstrap + + +class PygameAutomationBootstrap(PygameGuiBootstrap): + def app_source(self): + return """\ +import importlib.metadata +import os +import sys + +import pygame + +SCREEN_WIDTH, SCREEN_HEIGHT = 800, 600 +WHITE = (255, 255, 255) + + +def main(): + app_module = sys.modules["__main__"].__package__ + metadata = importlib.metadata.metadata(app_module) + + os.environ["SDL_VIDEO_X11_WMCLASS"] = metadata["Formal-Name"] + + pygame.init() + pygame.display.set_caption(metadata["Formal-Name"]) + screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) + + pygame.time.set_timer(pygame.QUIT, 2000) + + running = True + while running: + for event in pygame.event.get(): + if event.type == pygame.QUIT: + print(">>> successfully started...exiting <<<") + print(">>>>>>>>>> EXIT 0 <<<<<<<<<<") + running = False + break + + screen.fill(WHITE) + pygame.display.flip() + + pygame.quit() +""" diff --git a/automation/src/automation/bootstraps/pyside6.py b/automation/src/automation/bootstraps/pyside6.py new file mode 100644 index 000000000..cab34eb40 --- /dev/null +++ b/automation/src/automation/bootstraps/pyside6.py @@ -0,0 +1,40 @@ +from briefcase.bootstraps import PySide6GuiBootstrap + + +class PySide6AutomationBootstrap(PySide6GuiBootstrap): + def app_source(self): + return """\ +import importlib.metadata +import sys + +from PySide6 import QtWidgets +from PySide6.QtCore import QTimer + + +class {{ cookiecutter.class_name }}(QtWidgets.QMainWindow): + def __init__(self): + super().__init__() + self.init_ui() + + def init_ui(self): + self.setWindowTitle("{{ cookiecutter.app_name }}") + self.show() + + QTimer.singleShot(2000, self.exit_app) + + def exit_app(self): + print(">>> successfully started...exiting <<<") + print(">>>>>>>>>> EXIT 0 <<<<<<<<<<") + QtWidgets.QApplication.quit() + + +def main(): + app_module = sys.modules["__main__"].__package__ + metadata = importlib.metadata.metadata(app_module) + + QtWidgets.QApplication.setApplicationName(metadata["Formal-Name"]) + + app = QtWidgets.QApplication(sys.argv) + main_window = {{ cookiecutter.class_name }}() + sys.exit(app.exec()) +""" diff --git a/automation/src/automation/bootstraps/toga.py b/automation/src/automation/bootstraps/toga.py new file mode 100644 index 000000000..74d4448d2 --- /dev/null +++ b/automation/src/automation/bootstraps/toga.py @@ -0,0 +1,31 @@ +from briefcase.bootstraps import TogaGuiBootstrap + + +class TogaAutomationBootstrap(TogaGuiBootstrap): + def app_source(self): + return '''\ +import asyncio + +import toga + + +class {{ cookiecutter.class_name }}(toga.App): + + def startup(self): + """Construct and show the Toga application.""" + self.main_window = toga.MainWindow(title=self.formal_name) + self.main_window.show() + + self.add_background_task(self.exit_soon) + + async def exit_soon(self, app: toga.App, **kwargs): + """Background task that closes the app after a few seconds.""" + await asyncio.sleep(2) + print(">>> successfully started...exiting <<<") + print(">>>>>>>>>> EXIT 0 <<<<<<<<<<") + self.exit() + + +def main(): + return {{ cookiecutter.class_name }}() +''' diff --git a/changes/1549.misc.rst b/changes/1549.misc.rst new file mode 100644 index 000000000..4ff04c3bb --- /dev/null +++ b/changes/1549.misc.rst @@ -0,0 +1 @@ +The Briefcase Automation package was created to facilitate automated testing in CI; for example, starting apps built in CI that can automatically exit. diff --git a/pyproject.toml b/pyproject.toml index aaf4db8b9..c39288100 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,6 @@ [build-system] requires = [ + # keep versions in sync with automation/pyproject.toml "setuptools==69.0.0", "setuptools_scm==8.0.4", ] diff --git a/tox.ini b/tox.ini index 6e9a284db..a32bee416 100644 --- a/tox.ini +++ b/tox.ini @@ -127,12 +127,13 @@ commands = lint : python -m sphinx {[docs]sphinx_args_extra} -b linkcheck . {[docs]build_dir}/links all : python -m sphinx {[docs]sphinx_args_extra} -b html . {[docs]build_dir}/html -[testenv:package] +[testenv:package{,-with-automation}] skip_install = True passenv = FORCE_COLOR deps = build==1.0.3 twine==4.0.2 commands = - python -m build --outdir dist/ . + python -m build . --outdir dist/ + with-automation: python -m build automation/ --outdir dist/ python -m twine check dist/*