diff --git a/gooey/gui/application.py b/gooey/gui/application.py index c1ed1db..b8a4bbc 100644 --- a/gooey/gui/application.py +++ b/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) diff --git a/gooey/gui/containers/application.py b/gooey/gui/containers/application.py index 78d08c9..d0d59d1 100644 --- a/gooey/gui/containers/application.py +++ b/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) diff --git a/gooey/tests/__init__.py b/gooey/tests/__init__.py index e69de29..c2b3b5c 100644 --- a/gooey/tests/__init__.py +++ b/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() \ No newline at end of file diff --git a/gooey/tests/harness.py b/gooey/tests/harness.py index 1237936..bd5d289 100644 --- a/gooey/tests/harness.py +++ b/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 diff --git a/gooey/tests/test_application.py b/gooey/tests/test_application.py index dbacb77..ccc12a1 100644 --- a/gooey/tests/test_application.py +++ b/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): diff --git a/gooey/tests/test_argparse_to_json.py b/gooey/tests/test_argparse_to_json.py index 0f369e6..e1d1b4f 100644 --- a/gooey/tests/test_argparse_to_json.py +++ b/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): diff --git a/gooey/tests/test_chooser_results.py b/gooey/tests/test_chooser_results.py index fb86888..39e0094 100644 --- a/gooey/tests/test_chooser_results.py +++ b/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): diff --git a/gooey/tests/test_cmd_args.py b/gooey/tests/test_cmd_args.py index 9de7e74..70a74a0 100644 --- a/gooey/tests/test_cmd_args.py +++ b/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): diff --git a/gooey/tests/test_config_generator.py b/gooey/tests/test_config_generator.py index 9fd10d3..f2538b5 100644 --- a/gooey/tests/test_config_generator.py +++ b/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): diff --git a/gooey/tests/test_constraints.py b/gooey/tests/test_constraints.py index f5c1231..5654156 100644 --- a/gooey/tests/test_constraints.py +++ b/gooey/tests/test_constraints.py @@ -1,7 +1,7 @@ import unittest from gooey import GooeyParser - +from gooey.tests import * class TestConstraints(unittest.TestCase): diff --git a/gooey/tests/test_dropdown.py b/gooey/tests/test_dropdown.py index c65a730..4c2f11f 100644 --- a/gooey/tests/test_dropdown.py +++ b/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): diff --git a/gooey/tests/test_filterable_dropdown.py b/gooey/tests/test_filterable_dropdown.py index 1a8de91..faa55ba 100644 --- a/gooey/tests/test_filterable_dropdown.py +++ b/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 diff --git a/gooey/tests/test_header.py b/gooey/tests/test_header.py index 9368ed2..e63b46d 100644 --- a/gooey/tests/test_header.py +++ b/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): diff --git a/gooey/tests/test_parent_inheritance.py b/gooey/tests/test_parent_inheritance.py index 13d733e..a8b34ad 100644 --- a/gooey/tests/test_parent_inheritance.py +++ b/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): diff --git a/gooey/tests/test_radiogroup.py b/gooey/tests/test_radiogroup.py index d47372e..847271f 100644 --- a/gooey/tests/test_radiogroup.py +++ b/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. diff --git a/gooey/tests/test_time_remaining.py b/gooey/tests/test_time_remaining.py index 6a26089..edf98e7 100644 --- a/gooey/tests/test_time_remaining.py +++ b/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, diff --git a/gooey/tests/test_util.py b/gooey/tests/test_util.py index b0f7e36..469439d 100644 --- a/gooey/tests/test_util.py +++ b/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