Skip to content

Commit

Permalink
Add the first GUI test for the grid editor.
Browse files Browse the repository at this point in the history
This commit adds the `GuiTestCase` class, as well as an example UI test.
At the start of each test, a new instance of the `GridBlueprintControl` class
is created and displayed, and the test author can define the sequence of
actions to be simulated using `wx.UIActionSimulator`, and check that the
properties of the `GridBlueprintControl` instance are as expected. The test
can also be run in headless mode with `pytest-xvfb`.

Unfortunately, this test only works in certain environments (we've only
gotten it to work locally on Linux with pure X11 environments), so we
are disabling the tests for now.
  • Loading branch information
scottyak committed Jan 19, 2021
1 parent f67291f commit 51ea251
Show file tree
Hide file tree
Showing 4 changed files with 197 additions and 3 deletions.
9 changes: 8 additions & 1 deletion .coveragerc
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
[report]
# Don't worry about coverage for the grid GUI (for now)
[run]
omit =
armi/cli/gridGui.py
armi/utils/gridEditor.py
armi/utils/tests/test_gridGui.py

[report]
omit =
armi/cli/gridGui.py
armi/utils/gridEditor.py
armi/utils/tests/test_gridGui.py
185 changes: 185 additions & 0 deletions armi/utils/tests/test_gridGui.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
# Copyright 2021 Google, LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Tests for gridEditor.py.
By default, this would open the app in your primary display and steal control from your mouse and keyboard while the
test is running. This means that if your display is smaller than 1000 x 1000, or if you move your mouse when the test is
running, the test might fail even when things are fine.
These issues can be resolved by running the test in a virtual display. To do so, `pip install pytest-xvfb`,
and run `pytest test_gridGui.py` as usual. If you wish to change the resolution of the virtual display, you can modify
the `xvfb_width` and `xvfb_height` in `pytest.ini`.
After you have installed pytest-xvfb, you will no longer see the app displayed on your screen, which can make debugging
harder. Thus, during debugging, you may want the app to appear on your display by setting the `--no-xvfb` flag, and
have the print statements print to your console by setting the `--capture=tee-sys` flag, like this:
```
pytest --no-xvfb --capture=tee-sys test_gridGui.py
```
Note:
These tests currently require a rather specific environment:
1. wxPython needs to be installed, and
2. The test needs to run in a pure X11 environment (doesn't work in Wayland or XWayland, unfortunately).
To check if you are in an X11 environment, run this command:
```
loginctl list-sessions --no-legend | \
cut --delimiter=' ' --field=1 | \
xargs loginctl show-session --property=Type --value
```
If it outputs "x11", it should work (and if it outputs "wayland", it probably won't, for now).
"""
import asyncio
import os
import pytest
import time
import unittest
import wx

import armi

if armi._app is None:
armi.configure()
from armi.utils import gridEditor

_SECONDS_PER_TICK = 0.05


def _wait(num_ticks: int) -> None:
time.sleep(num_ticks * _SECONDS_PER_TICK)


def _findPointInWindow(
window: wx.Window, offsetFromLeft: float = 0.5, offsetFromTop: float = 0.5
) -> wx.Point:
"""Given a window, return a point in it. Defaults to the center of the window object.
If offsets are smaller than 0 or greater than 1, this would return a point outside the window object.
"""
rect: wx.Rect = window.GetScreenRect()
x = rect.x + int(offsetFromLeft * rect.width)
if x == rect.x + rect.width:
x = rect.x + rect.width - 1
y = rect.y + int(offsetFromTop * rect.height)
if y == rect.y + rect.height:
y = rect.y + rect.height - 1
return wx.Point(x, y)


class GuiTestCase(unittest.TestCase):
"""Provides scaffolding for a GUI test.
Without this scaffolding, the GUI's main loop would block the UIActionSimulator. Thus, the simulated actions and
asserts must be run asynchronously within the GUI's event loop. Since the asserts are also run asynchronously,
we need to make sure that the test does not end until all assert statements have been called, and that the test
outputs are properly passed to the test framework. The app is also properly torn down after each test.
This way, the user only needs to define the simulated actions and the expected behavior in order to write a UI test.
"""

def initializeGui(self):
"""The user can override this to initialize the GUI differently.
Note: This method is called in self.run(), before super().run. We deliberately avoid naming this 'setUp',
because super().run internally calls self.setUp, which would be too late.
"""

self.app = wx.App()
self.frame = wx.Frame(
None, wx.ID_ANY, title="Grid Blueprints UI", pos=(0, 0), size=(1000, 1000)
)
self.gui = gridEditor.GridBlueprintControl(self.frame)
self.frame.Show()
self.inputSimulator = wx.UIActionSimulator()

def _cleanUpApp(self):
for window in wx.GetTopLevelWindows():
try:
assert window.IsModal()
except (AttributeError, AssertionError):
window and window.Close()
else:
window.EndModal(0)
self.app.ScheduleForDestruction(window)

def _runAsync(self, result):
super().run(result)
self._cleanUpApp()
self._testCompleted.set_result(None)

def run(self, result=None):
"""Overrides unittest.TestCase.run."""
self.initializeGui()
loop = asyncio.get_event_loop()
self._testCompleted = loop.create_future()
wx.CallLater(0, self._runAsync, result)
self.app.MainLoop()
loop.run_until_complete(self._testCompleted)
return result


@pytest.mark.skipif(
not bool(os.environ.get("ARMI_GUI_TESTS", False)),
reason="GUI tests require a rather specific environment (see above), so these tests are opt-in",
)
class Test(GuiTestCase):
def test_setNumRings(self):
# Set the number of rings to 1
self.inputSimulator.MouseMove(
_findPointInWindow(self.gui.controls.ringControl, offsetFromLeft=0.15)
)
_wait(num_ticks=5)
self.inputSimulator.MouseDblClick()
_wait(num_ticks=5)
self.inputSimulator.KeyDown(49) # 49 is the keycode for the "1" key
_wait(num_ticks=1)
self.inputSimulator.KeyUp(49)
_wait(num_ticks=5)

# Select (i, j) specifier
self.inputSimulator.MouseMove(_findPointInWindow(self.gui.controls.labelMode))
_wait(num_ticks=5)
self.inputSimulator.MouseDown()
_wait(num_ticks=1)
self.inputSimulator.MouseUp()
_wait(num_ticks=5)
self.inputSimulator.MouseMove(
_findPointInWindow(self.gui.controls.labelMode, offsetFromTop=1.5)
)
_wait(num_ticks=5)
self.inputSimulator.MouseDown()
_wait(num_ticks=1)
self.inputSimulator.MouseUp()
_wait(num_ticks=5)

# Click the Apply button
self.inputSimulator.MouseMove(_findPointInWindow(self.gui.controls.ringApply))
_wait(num_ticks=5)
self.inputSimulator.MouseDown()
_wait(num_ticks=1)
self.inputSimulator.MouseUp()
_wait(num_ticks=5)

# Assert that there is only one grid cell
gridCellIndices = self.gui.clicker.indicesToPdcId
self.assertEqual(1, len(gridCellIndices))

# Assert that the grid cell contains "0, 0'
labels = [self.gui.clicker._getLabel(idx)[0] for idx in gridCellIndices]
self.assertEqual("0, 0", labels[0])


if __name__ == "__main__":
unittest.main()
2 changes: 2 additions & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ addopts = --durations=30 --tb=native
filterwarnings =
ignore:\s*the matrix subclass is not the recommended way:PendingDeprecationWarning
ignore:\s*Loading from XML-format settings:DeprecationWarning
xvfb_width = 1200
xvfb_height = 1050
4 changes: 2 additions & 2 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@ deps=
setenv =
PYTHONPATH = {toxinidir}
commands =
pytest {posargs} armi
pytest --ignore=armi/utils/tests/test_gridGui.py {posargs} armi

[testenv:cov]
deps=
-r{toxinidir}/requirements.txt
-r{toxinidir}/requirements-testing.txt
commands =
pytest --cov-config=.coveragerc --cov=armi {posargs} armi
pytest --cov-config=.coveragerc --cov=armi --ignore=armi/utils/tests/test_gridGui.py {posargs} armi

[testenv:lint]
ignore_errors = true
Expand Down

0 comments on commit 51ea251

Please sign in to comment.