Browse Source

reuse wx.App instance to increase test reliability

1.0.5-release-candidate
Chris 3 years ago
parent
commit
6e5e28dcdc
17 changed files with 140 additions and 60 deletions
  1. 33
      gooey/gui/application.py
  2. 55
      gooey/gui/containers/application.py
  3. 43
      gooey/tests/__init__.py
  4. 20
      gooey/tests/harness.py
  5. 21
      gooey/tests/test_application.py
  6. 2
      gooey/tests/test_argparse_to_json.py
  7. 2
      gooey/tests/test_chooser_results.py
  8. 2
      gooey/tests/test_cmd_args.py
  9. 2
      gooey/tests/test_config_generator.py
  10. 2
      gooey/tests/test_constraints.py
  11. 2
      gooey/tests/test_dropdown.py
  12. 2
      gooey/tests/test_filterable_dropdown.py
  13. 2
      gooey/tests/test_header.py
  14. 2
      gooey/tests/test_parent_inheritance.py
  15. 4
      gooey/tests/test_radiogroup.py
  16. 4
      gooey/tests/test_time_remaining.py
  17. 2
      gooey/tests/test_util.py

33
gooey/gui/application.py

@ -18,19 +18,26 @@ from gooey.util.functional import merge
def run(build_spec):
app, _ = build_app(build_spec)
app.MainLoop()
app, _ = build_app(build_spec)
app.MainLoop()
def build_app(build_spec):
app = wx.App(False)
# use actual program name instead of script file name in macOS menu
app.SetAppDisplayName(build_spec['program_name'])
i18n.load(build_spec['language_dir'], build_spec['language'], build_spec['encoding'])
imagesPaths = image_repository.loadImages(build_spec['image_dir'])
gapp = GooeyApplication(merge(build_spec, imagesPaths))
wx.lib.inspection.InspectionTool().Show()
gapp.Show()
return (app, gapp)
app = wx.App(False)
return _build_app(build_spec, app)
def _build_app(build_spec, app):
"""
Note: this method is broken out with app as
an argument to facilitate testing.
"""
# use actual program name instead of script file name in macOS menu
app.SetAppDisplayName(build_spec['program_name'])
i18n.load(build_spec['language_dir'], build_spec['language'], build_spec['encoding'])
imagesPaths = image_repository.loadImages(build_spec['image_dir'])
gapp = GooeyApplication(merge(build_spec, imagesPaths))
# wx.lib.inspection.InspectionTool().Show()
gapp.Show()
return (app, gapp)

55
gooey/gui/containers/application.py

@ -95,8 +95,6 @@ class GooeyApplication(wx.Frame):
config.displayErrors()
self.Layout()
def onEdit(self):
"""Return the user to the settings screen for further editing"""
with transactUI(self):
@ -144,14 +142,46 @@ class GooeyApplication(wx.Frame):
if self.buildSpec.get('show_failure_modal'):
wx.CallAfter(modals.showFailure)
def onCancel(self):
"""Close the program after confirming
We treat the behavior of the "cancel" button slightly
differently than the general window close X button only
because this is 'part of' the form.
"""
if modals.confirmExit():
self.onClose()
def onStopExecution(self):
"""Displays a scary message and then force-quits the executing
client code if the user accepts"""
if not self.buildSpec['show_stop_warning'] or modals.confirmForceStop():
if self.shouldStopExecution():
self.clientRunner.stop()
def onClose(self, *args, **kwargs):
"""Stop any actively running client program, cleanup the top
level WxFrame and shutdown the current process"""
# issue #592 - we need to run the same onStopExecution machinery
# when the exit button is clicked to ensure everything is cleaned
# up correctly.
if self.clientRunner.running():
if self.shouldStopExecution():
self.clientRunner.stop()
self.destroyGooey()
else:
self.destroyGooey()
def shouldStopExecution(self):
return not self.buildSpec['show_stop_warning'] or modals.confirmForceStop()
def destroyGooey(self):
self.Destroy()
sys.exit()
def fetchExternalUpdates(self):
"""
!Experimental!
@ -165,25 +195,6 @@ class GooeyApplication(wx.Frame):
for config in self.configs:
config.seedUI(seeds)
def onCancel(self):
"""Close the program after confirming"""
if modals.confirmExit():
self.onClose()
def onClose(self, *args, **kwargs):
"""Stop any actively running client program, cleanup the top
level WxFrame and shutdown the current process"""
# issue #592 - we need to run the same onStopExecution machinery
# when the exit button is clicked to ensure everything is cleaned
# up correctly.
if self.clientRunner.running():
self.onStopExecution()
self.Destroy()
sys.exit()
def layoutComponent(self):
sizer = wx.BoxSizer(wx.VERTICAL)
sizer.Add(self.header, 0, wx.EXPAND)

43
gooey/tests/__init__.py

@ -0,0 +1,43 @@
"""
This weirdness exists to work around a very specific problem
with testing WX: you can only ever have one App() instance per
process. I've spent hours and hours trying to work around this and
figure out how to gracefully destroy and recreate them, but... no dice.
This is echo'd in the docs: https://wxpython.org/Phoenix/docs/html/wx.App.html
Destroying/recreating causes instability in the tests. We can work around that
by reusing the same App instance across tests and only destroying the top level
frame (which is fine). However, this causes a new problem: the last test which
runs will now always fail, cause we're not Destroying the App instance.
Ideal world: UnitTest would expose a global "done" hook regardless of test
discovery type. That doesn't exist, so the best we can do is use the Module cleanup
methods. These aren't perfect, but destroying / recreating at the module boundary
gives slightly more reliable tests. These are picked up by the test runner
by their existence in the module's globals(). There's no other way to hook
things together. We need it in every test, and thus... that's the background
for why this weirdness is going on.
It's a hack around a hack around a problem in Wx.
Usage:
In any tests which use WX, you must import this module's definitions
into the test's global scope
```
from gooey.tests import *
```
"""
import wx
app = None
def setUpModule():
global app
app = wx.App()
def tearDownModule():
global app
app.Destroy()

20
gooey/tests/harness.py

@ -1,24 +1,38 @@
from contextlib import contextmanager
import time
from threading import Thread
import wx
from gui import application
from gooey.gui import application
from python_bindings.config_generator import create_from_parser
from python_bindings.gooey_decorator import defaults
from util.functional import merge
@contextmanager
def instrumentGooey(parser, **kwargs):
"""
Context manager used during testing for setup/tear down of the
WX infrastructure during subTests.
Weirdness warning: this uses a globally reused wx.App instance.
"""
from gooey.tests import app
if app == None:
raise Exception("App instance has not been created! This is likely due to "
"you forgetting to add the magical import which makes all these "
"tests work. See the module doc in gooey.tests.__init__ for guidance")
buildspec = create_from_parser(parser, "", **merge(defaults, kwargs))
app, gooey = application.build_app(buildspec)
app, gooey = application._build_app(buildspec, app)
app.SetTopWindow(gooey)
try:
yield (app, gooey)
finally:
wx.CallAfter(app.ExitMainLoop)
gooey.Destroy()
app.Destroy()
app.SetTopWindow(None)
del gooey

21
gooey/tests/test_application.py

@ -8,11 +8,10 @@ from unittest.mock import MagicMock
from python_bindings import constants
from tests.harness import instrumentGooey
from gooey.tests import *
class TestGooeyApplication(unittest.TestCase):
def testFullscreen(self):
parser = self.basicParser()
for shouldShow in [True, False]:
@ -64,16 +63,14 @@ class TestGooeyApplication(unittest.TestCase):
"""
parser = self.basicParser()
with instrumentGooey(parser) as (app, gapp):
with patch('gui.containers.application.sys.exit') as exitmock:
gapp.clientRunner = MagicMock()
gapp.Destroy = MagicMock()
# mocking that the user clicks "yes shut down" in the warning modal
mockModal.return_value = True
gapp.onClose()
mockModal.assert_called()
gapp.Destroy.assert_called()
exitmock.assert_called()
gapp.clientRunner = MagicMock()
gapp.destroyGooey = MagicMock()
# mocking that the user clicks "yes shut down" in the warning modal
mockModal.return_value = True
gapp.onClose()
mockModal.assert_called()
gapp.destroyGooey.assert_called()
def testTerminalColorChanges(self):

2
gooey/tests/test_argparse_to_json.py

@ -6,7 +6,7 @@ from argparse import ArgumentParser
from gooey import GooeyParser
from gooey.python_bindings import argparse_to_json
from gooey.util.functional import getin
from gooey.tests import *
class TestArgparse(unittest.TestCase):

2
gooey/tests/test_chooser_results.py

@ -3,6 +3,8 @@ import os
import unittest
from gooey.gui.components.widgets.core import chooser
from gooey.tests import *
class MockWxMDD:
def GetPaths(self):

2
gooey/tests/test_cmd_args.py

@ -3,6 +3,8 @@ import unittest
from gooey import GooeyParser
from gooey.python_bindings import cmd_args
from argparse import ArgumentParser
from gooey.tests import *
class TextCommandLine(unittest.TestCase):

2
gooey/tests/test_config_generator.py

@ -3,7 +3,7 @@ from argparse import ArgumentParser
from python_bindings import constants
from python_bindings.config_generator import create_from_parser
from gooey.tests import *
class TextConfigGenerator(unittest.TestCase):

2
gooey/tests/test_constraints.py

@ -1,7 +1,7 @@
import unittest
from gooey import GooeyParser
from gooey.tests import *
class TestConstraints(unittest.TestCase):

2
gooey/tests/test_dropdown.py

@ -3,7 +3,7 @@ from argparse import ArgumentParser
from unittest.mock import patch
from tests.harness import instrumentGooey
from gooey.tests import *
class TestGooeyDropdown(unittest.TestCase):

2
gooey/tests/test_filterable_dropdown.py

@ -2,8 +2,8 @@ import unittest
from argparse import ArgumentParser
from collections import namedtuple
from unittest.mock import patch
import wx
from gooey.tests import *
from gooey.tests.harness import instrumentGooey
from gooey import GooeyParser

2
gooey/tests/test_header.py

@ -3,7 +3,7 @@ from argparse import ArgumentParser
from itertools import *
from tests.harness import instrumentGooey
from gooey.tests import *
class TestGooeyHeader(unittest.TestCase):

2
gooey/tests/test_parent_inheritance.py

@ -2,7 +2,7 @@ import argparse
import unittest
from gooey import GooeyParser
from gooey.tests import *
class TestParentInheritance(unittest.TestCase):

4
gooey/tests/test_radiogroup.py

@ -1,13 +1,13 @@
import unittest
import wx
from gooey import GooeyParser
from gooey.tests import *
from tests.harness import instrumentGooey
class TestRadioGroupBehavior(unittest.TestCase):
def mutext_group(self, options):
"""
Basic radio group consisting of two options.

4
gooey/tests/test_time_remaining.py

@ -5,6 +5,9 @@ from itertools import *
from tests.harness import instrumentGooey
from gooey.tests import *
class TestFooterTimeRemaining(unittest.TestCase):
def make_parser(self):
@ -18,7 +21,6 @@ class TestFooterTimeRemaining(unittest.TestCase):
gooeyApp.showConsole()
footer = gooeyApp.footer
self.assertEqual(
footer.time_remaining_text.Shown,

2
gooey/tests/test_util.py

@ -1,6 +1,8 @@
import re
import unittest
from gooey.tests import *
from gooey.gui.util.time import get_current_time,get_elapsed_time,estimate_time_remaining,format_interval

Loading…
Cancel
Save