Skip to content

Commit

Permalink
Merge pull request #1534 from bruno-rino/add_dummy_platform
Browse files Browse the repository at this point in the history
Add `dummy` as a known platform, and use it in tests
  • Loading branch information
freakboy3742 committed Oct 26, 2022
2 parents ba970f3 + 9491665 commit 27ead8b
Show file tree
Hide file tree
Showing 130 changed files with 1,390 additions and 630 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,12 +87,12 @@ jobs:
- runs-on: ubuntu-latest
- python-version: "3.7" # Should be env.min_python_version (https://github.com/actions/runner/issues/480)
- pre-command:
- test-command: pytest
- test-command: pytest -v
- backend: cocoa
runs-on: macos-latest
- backend: gtk
pre-command: "sudo apt-get update -y && sudo apt-get install -y python3-gi python3-gi-cairo gir1.2-gtk-3.0 python3-dev libgirepository1.0-dev libcairo2-dev pkg-config"
test-command: "xvfb-run -a -s '-screen 0 2048x1536x24' pytest"
test-command: "xvfb-run -a -s '-screen 0 2048x1536x24' pytest -v"
- backend: iOS
runs-on: macos-latest
- backend: winforms
Expand Down Expand Up @@ -124,4 +124,4 @@ jobs:
- name: Test
run: |
cd src/${{ matrix.backend }}
${{ matrix.test-command }}
TOGA_BACKEND=toga_${{ matrix.backend }} ${{ matrix.test-command }}
2 changes: 1 addition & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
version = '.'.join(release.split('.')[:2])

# Fix the autodoc import issues
os.environ['TOGA_PLATFORM'] = 'dummy'
os.environ['TOGA_BACKEND'] = 'toga_dummy'

autoclass_content = 'both'

Expand Down
102 changes: 26 additions & 76 deletions docs/how-to/contribute.rst
Original file line number Diff line number Diff line change
Expand Up @@ -364,7 +364,7 @@ Start by running the core test suite:
.. code-block:: bash
(venv) $ cd src/core
(venv) $ python setup.py test
(venv) $ TOGA_BACKEND=toga_dummy python setup.py test
...
----------------------------------------------------------------------
Ran 181 tests in 0.343s
Expand All @@ -376,7 +376,7 @@ Start by running the core test suite:
.. code-block:: bash
(venv) $ cd src/core
(venv) $ python setup.py test
(venv) $ TOGA_BACKEND=toga_dummy python setup.py test
...
----------------------------------------------------------------------
Ran 181 tests in 0.343s
Expand All @@ -388,20 +388,30 @@ Start by running the core test suite:
.. code-block:: doscon
(venv) C:\...>cd src/core
(venv) C:\...>set TOGA_BACKEND=toga_dummy
(venv) C:\...>python setup.py test
(venv) C:\...>set TOGA_BACKEND=
...
----------------------------------------------------------------------
Ran 181 tests in 0.343s
OK (skipped=1)
You should get some output indicating that tests have been run. You shouldnt
You should get some output indicating that tests have been run. You shouldn't
ever get any FAIL or ERROR test results. We run our full test suite before
merging every patch. If that process discovers any problems, we dont merge
the patch. If you do find a test error or failure, either theres something
odd in your test environment, or youve found an edge case that we havent
merging every patch. If that process discovers any problems, we don't merge
the patch. If you do find a test error or failure, either there's something
odd in your test environment, or you've found an edge case that we haven't
seen before - either way, let us know!
Note that when we run the test suite, we set the environment variable
``TOGA_BACKEND``. Under normal operation, Toga will automatically choose the
appropriate backend for your platform. However, when running the tests for
the core platform, we need to use a special "dummy" backend. This dummy backend
satisfies the interface contract of a Toga backend, but doesn't acutally
display any widgets. This allows us to test the behavior of the core library
independent of the behavior of a specific backend.
Although the tests should all pass, the test suite itself is still
incomplete. There are many aspects of the Toga Core API that aren't currently
tested (or aren't tested thoroughly). To work out what *isn't* tested, we're
Expand All @@ -421,7 +431,7 @@ ask coverage to generate a report of the data that was gathered:
.. code-block:: bash
(venv) $ pip install coverage
(venv) $ coverage run setup.py test
(venv) $ TOGA_BACKEND=toga_dummy coverage run setup.py test
(venv) $ coverage report -m --include="toga/*"
Name Stmts Miss Cover Missing
------------------------------------------------------------------
Expand All @@ -437,7 +447,7 @@ ask coverage to generate a report of the data that was gathered:
.. code-block:: bash
(venv) $ pip install coverage
(venv) $ coverage run setup.py test
(venv) $ TOGA_BACKEND=toga_dummy coverage run setup.py test
(venv) $ coverage report -m --include="toga/*"
Name Stmts Miss Cover Missing
------------------------------------------------------------------
Expand All @@ -453,7 +463,9 @@ ask coverage to generate a report of the data that was gathered:
.. code-block:: doscon
(venv) C:\...>pip install coverage
(venv) C:\...>set TOGA_BACKEND=toga_dummy
(venv) C:\...>coverage run setup.py test
(venv) C:\...>set TOGA_BACKEND=
(venv) C:\...>coverage report -m --include=toga/*
Name Stmts Miss Cover Missing
------------------------------------------------------------------
Expand Down Expand Up @@ -488,7 +500,7 @@ expect to see something like:
.. code-block:: bash
(venv) $ coverage run setup.py test
(venv) $ TOGA_BACKEND=toga_dummy coverage run setup.py test
running test
...
----------------------------------------------------------------------
Expand All @@ -509,7 +521,7 @@ expect to see something like:
.. code-block:: bash
(venv) $ coverage run setup.py test
(venv) $ TOGA_BACKEND=toga_dummy coverage run setup.py test
running test
...
----------------------------------------------------------------------
Expand All @@ -530,7 +542,9 @@ expect to see something like:
.. code-block:: doscon
(venv) C:\...>set TOGA_BACKEND=toga_dummy
(venv) C:\...>coverage run setup.py test
(venv) C:\...>set TOGA_BACKEND=
running test
...
----------------------------------------------------------------------
Expand All @@ -554,70 +568,6 @@ in the coverage results.
Submit a pull request for your work, and you're done! Congratulations, you're
a contributor to Toga!
How does this all work?
=======================
Since you're writing tests for a GUI toolkit, you might be wondering why you
haven't seen a GUI yet. The Toga Core package contains the API definitions for
the Toga widget kit. This is completely platform agnostic - it just provides
an interface, and defers actually drawing anything on the screen to the
platform backends.
When you run the test suite, the test runner uses a "dummy" backend - a
platform backend that *implements* the full API, but doesn’t actually *do*
anything (i.e., when you say display a button, it creates an object, but
doesn’t actually display a button).
In this way, it's possible to for the Toga Core tests to exercise every API
entry point in the Toga Core package, verify that data is stored correctly on
the interface layer, and sent through to the right endpoints in the Dummy
backend. If the *dummy* backend is invoked correctly, then any other backend
will be handled correctly, too.
One error you might see...
--------------------------
When you're running these tests - especially when you submit your PR, and the
tests run on our continuous integration (CI) server - it's possible you might get
an error that reads::
ModuleNotFoundError: No module named 'toga_gtk'.
If this happens, you've found an bug in the way the widget you're testing
has been constructed.
The Core API is designed to be platform independent. When a widget is created,
it calls upon a "factory" to instantiate the underlying platform-dependent
implementation. When a Toga application starts running, it will try to guess
the right factory to use based on the environment where the code is running.
So, if you run your code on a Mac, it will use the Cocoa factory; if you're on
a Linux box, it will use the GTK factory.
However, when writing tests, we want to use the "dummy" factory. The Dummy
factory isn't the "native" platform anywhere - it's just a placeholder. As a
result, the dummy factory won't be used unless you specifically request it -
which means every widget has to honor that request.
Most Toga widgets create their platform-specific implementation when they are
created. As a result, most Toga widgets should accept a ``factory`` argument -
and that factory should be used to instantiate any widget implementations or
sub-widgets.
However, *some* widgets - like Icon - are "late loaded" - the implementation
isn't created until the widget is actually *used*. Late loaded widgets don't
accept a ``factory`` when they're created - but they *do* have an `_impl()`
method that accepts a factory.
If these factory arguments aren't being passed around correctly, then a test
suite will attempt to create a widget, but will fall back to the platform-
default factory, rather than the "dummy" factory. If you've installed the
appropriate platform default backend, you won't (necessarily) get an error,
but your tests won't use the dummy backend. On our CI server, we deliberately
don't install a platform backend so we can find these errors.
If you get the ``ModuleNotFoundError``, you need to audit the code to find out
where a widget is being created without a factory being specified.
It's not just about coverage!
=============================
Expand All @@ -629,8 +579,8 @@ purpose it was intended!
As you develop tests and improve coverage, you should be checking that the
core module is internally **consistent** as well. If you notice any method
names that arent internally consistent (e.g., something called ``on_select``
in one module, but called ``on_selected`` in another), or where the data isnt
names that aren't internally consistent (e.g., something called ``on_select``
in one module, but called ``on_selected`` in another), or where the data isn't
being handled consistently (one widget updates then refreshes, but another
widget refreshes then updates), flag it and bring it to our attention by
raising a ticket. Or, if you're confident that you know what needs to be done,
Expand Down
4 changes: 4 additions & 0 deletions nursery/curses/setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ include =
toga_curses
toga_curses.*

[options.entry_points]
toga.backends =
terminal = toga_curses

[flake8]
exclude=\
.eggs/*,\
Expand Down
6 changes: 6 additions & 0 deletions nursery/qt/setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ include =
toga_qt
toga_qt.*

[options.entry_points]
toga.backends =
linux = toga_qt
macOS = toga_qt
windows = toga_qt

[flake8]
exclude=\
.eggs/*,\
Expand Down
4 changes: 4 additions & 0 deletions nursery/tvOS/setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ include =
toga_tvOS
toga_tvOS.*

[options.entry_points]
toga.backends =
tvOS = toga_tvOS

[flake8]
exclude=\
.eggs/*,\
Expand Down
4 changes: 4 additions & 0 deletions nursery/uwp/setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ include =
toga_uwp
toga_uwp.*

[options.entry_points]
toga.backends =
windows = toga_uwp

[flake8]
exclude=\
.eggs/*,\
Expand Down
4 changes: 4 additions & 0 deletions nursery/watchOS/setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ include =
toga_watchOS
toga_watchOS.*

[options.entry_points]
toga.backends =
watchOS = toga_watchOS

[flake8]
exclude=\
.eggs/*,\
Expand Down
4 changes: 4 additions & 0 deletions src/android/setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ package_dir =
python_requires = >= 3.6
zip_safe = False

[options.entry_points]
toga.backends =
android = toga_android

[options.packages.find]
where = src

Expand Down
2 changes: 1 addition & 1 deletion src/android/src/toga_android/widgets/button.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def set_enabled(self, value):

def set_font(self, font):
if font:
font_impl = font.bind(self.interface.factory)
font_impl = font.bind()
self.native.setTextSize(TypedValue.COMPLEX_UNIT_SP, font_impl.get_size())
self.native.setTypeface(font_impl.get_typeface(), font_impl.get_style())

Expand Down
2 changes: 1 addition & 1 deletion src/android/src/toga_android/widgets/detailedlist.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ def _make_row(self, container, i):
icon_image_view = ImageView(self._native_activity)
icon = self.interface.data[i].icon
if icon is not None:
icon.bind(self.interface.factory)
icon.bind()
bitmap = BitmapFactory.decodeFile(str(icon._impl.path))
icon_image_view.setImageBitmap(bitmap)
icon_layout_params = RelativeLayout__LayoutParams(
Expand Down
2 changes: 1 addition & 1 deletion src/android/src/toga_android/widgets/label.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def set_text(self, value):

def set_font(self, font):
if font:
font_impl = font.bind(self.interface.factory)
font_impl = font.bind()
self.native.setTextSize(TypedValue.COMPLEX_UNIT_SP, font_impl.get_size())
self.native.setTypeface(font_impl.get_typeface(), font_impl.get_style())

Expand Down
2 changes: 1 addition & 1 deletion src/android/src/toga_android/widgets/multilinetextinput.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ def set_color(self, color):

def set_font(self, font):
if font:
font_impl = font.bind(self.interface.factory)
font_impl = font.bind()
self.native.setTextSize(TypedValue.COMPLEX_UNIT_SP, font_impl.get_size())
self.native.setTypeface(font_impl.get_typeface(), font_impl.get_style())

Expand Down
2 changes: 1 addition & 1 deletion src/android/src/toga_android/widgets/numberinput.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ def set_alignment(self, value):

def set_font(self, font):
if font:
font_impl = font.bind(self.interface.factory)
font_impl = font.bind()
self.native.setTextSize(TypedValue.COMPLEX_UNIT_SP, font_impl.get_size())
self.native.setTypeface(font_impl.get_typeface(), font_impl.get_style())

Expand Down
2 changes: 1 addition & 1 deletion src/android/src/toga_android/widgets/switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def get_value(self):

def set_font(self, font):
if font:
font_impl = font.bind(self.interface.factory)
font_impl = font.bind()
self.native.setTextSize(TypedValue.COMPLEX_UNIT_SP, font_impl.get_size())
self.native.setTypeface(font_impl.get_typeface(), font_impl.get_style())

Expand Down
2 changes: 1 addition & 1 deletion src/android/src/toga_android/widgets/table.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ def remove_column(self, accessor):

def set_font(self, font):
if font:
self._font_impl = font.bind(self.interface.factory)
self._font_impl = font.bind()
if self.interface.data is not None:
self.change_source(self.interface.data)

Expand Down
2 changes: 1 addition & 1 deletion src/android/src/toga_android/widgets/textinput.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ def set_alignment(self, value):

def set_font(self, font):
if font:
font_impl = font.bind(self.interface.factory)
font_impl = font.bind()
self.native.setTextSize(TypedValue.COMPLEX_UNIT_SP, font_impl.get_size())
self.native.setTypeface(font_impl.get_typeface(), font_impl.get_style())

Expand Down
4 changes: 4 additions & 0 deletions src/cocoa/setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ package_dir =
python_requires = >= 3.6
zip_safe = False

[options.entry_points]
toga.backends =
macOS = toga_cocoa

[options.packages.find]
where = src

Expand Down
4 changes: 2 additions & 2 deletions src/cocoa/src/toga_cocoa/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ def create(self):
self.native = NSApplication.sharedApplication
self.native.setActivationPolicy(NSApplicationActivationPolicyRegular)

icon = self.interface.icon.bind(self.interface.factory)
icon = self.interface.icon.bind()
self.native.setApplicationIconImage_(icon.native)

self.resource_path = os.path.dirname(os.path.dirname(NSBundle.mainBundle.bundlePath))
Expand Down Expand Up @@ -350,7 +350,7 @@ def set_main_window(self, window):
def show_about_dialog(self):
options = NSMutableDictionary.alloc().init()

options[NSAboutPanelOptionApplicationIcon] = self.interface.icon.bind(self.interface.factory).native
options[NSAboutPanelOptionApplicationIcon] = self.interface.icon.bind().native

if self.interface.name is not None:
options[NSAboutPanelOptionApplicationName] = self.interface.name
Expand Down
2 changes: 1 addition & 1 deletion src/cocoa/src/toga_cocoa/widgets/button.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def create(self):

def set_font(self, font):
if font:
self.native.font = font.bind(self.interface.factory).native
self.native.font = font.bind().native

def set_text(self, text):
self.native.title = self.interface.text
Expand Down

0 comments on commit 27ead8b

Please sign in to comment.