Source code for envisage.ui.tasks.tests.test_tasks_application
# (C) Copyright 2007-2023 Enthought, Inc., Austin, TX
# All rights reserved.
#
# This software is provided without warranty under the terms of the BSD
# license included in LICENSE.txt and may be redistributed only under
# the conditions described in the aforementioned license. The license
# is also available online at http://www.enthought.com/licenses/BSD.txt
#
# Thanks for using Enthought open source!
import os
import pathlib
import shutil
import sys
import tempfile
import unittest
import pkg_resources
from pyface.gui import GUI
from pyface.i_gui import IGUI
from traits.api import Event, HasTraits, provides
from envisage.api import Plugin
from envisage.tests.support import (
pyside6_available,
pyside6_version,
requires_gui,
)
from envisage.ui.tasks.api import TasksApplication
from envisage.ui.tasks.tasks_application import DEFAULT_STATE_FILENAME
# There's a PySide6 end-of-process segfault on Linux that's
# interfering with our CI runs, so we skip the relevant tests
# when running under GitHub Actions CI.
# xref: enthought/envisage#476
skip_with_flaky_pyside = unittest.skipIf(
(
os.getenv("GITHUB_ACTIONS") == "true"
and sys.platform == "linux"
and pyside6_available
and pyside6_version < (6, 4, 3)
),
"Skipping segfault-causing test on Linux. See enthought/envisage#476",
)
[docs]@provides(IGUI)
class DummyGUI(HasTraits):
pass
[docs]class LifecycleRecordingPlugin(Plugin):
"""
Plugin that fires events when started and stopped.
"""
#: Event fired when plugin starts
started = Event()
#: Event fired when plugin stops
stopped = Event()
[docs] def start(self):
self.started = True
[docs] def stop(self):
self.stopped = True
[docs]class LifecycleRecordingGUI(GUI):
"""
GUI subclass that adds events for watching start and stop of event loop.
"""
#: Event fired just before we start the event loop.
starting = Event
#: Event fired just after we've exited the event loop.
stopped = Event
[docs] def start_event_loop(self):
"""
Extend the base class method to fire the additional events.
"""
self.starting = True
super().start_event_loop()
self.stopped = True
[docs]@requires_gui
class TestTasksApplication(unittest.TestCase):
[docs] def setUp(self):
self.tmpdir = tempfile.mkdtemp()
self.addCleanup(shutil.rmtree, self.tmpdir)
[docs] @skip_with_flaky_pyside
def test_layout_save_with_protocol_3(self):
# Test that the protocol can be overridden on a per-application basis.
state_location = self.tmpdir
# Create application, and set it up to exit as soon as it's launched.
app = TasksApplication(
state_location=state_location,
layout_save_protocol=3,
)
app.on_trait_change(app.exit, "application_initialized")
memento_file = os.path.join(state_location, app.state_filename)
self.assertFalse(os.path.exists(memento_file))
app.run()
self.assertTrue(os.path.exists(memento_file))
# Check that the generated file uses protocol 3.
with open(memento_file, "rb") as f:
protocol_bytes = f.read(2)
self.assertEqual(protocol_bytes, b"\x80\x03")
[docs] @skip_with_flaky_pyside
def test_layout_save_creates_directory(self):
# Test that state can still be saved if the target directory
# doesn't exist.
state_location = pathlib.Path(self.tmpdir) / "subdir"
state_filename = "memento_test"
state_path = state_location / state_filename
self.assertFalse(state_location.exists())
self.assertFalse(state_path.exists())
# Create application and set it up to exit as soon as it's launched.
app = TasksApplication(
state_location=state_location,
state_filename=state_filename,
)
app.on_trait_change(app.exit, "application_initialized")
app.run()
self.assertTrue(state_location.exists())
self.assertTrue(state_path.exists())
[docs] @skip_with_flaky_pyside
def test_layout_load(self):
# Check we can load a previously-created state. That previous state
# has an main window size of (492, 743) (to allow us to check that
# we're actually using the file).
stored_state_location = pkg_resources.resource_filename(
"envisage.ui.tasks.tests", "data"
)
state_location = self.tmpdir
shutil.copyfile(
os.path.join(stored_state_location, "application_memento_v2.pkl"),
os.path.join(state_location, DEFAULT_STATE_FILENAME),
)
app = TasksApplication(state_location=state_location)
app.on_trait_change(app.exit, "application_initialized")
app.run()
state = app._state
self.assertEqual(state.previous_window_layouts[0].size, (492, 743))
[docs] @skip_with_flaky_pyside
def test_layout_load_pickle_protocol_3(self):
# Same as the above test, but using a state stored with pickle
# protocol 3.
stored_state_location = pkg_resources.resource_filename(
"envisage.ui.tasks.tests", "data"
)
state_location = self.tmpdir
shutil.copyfile(
os.path.join(stored_state_location, "application_memento_v3.pkl"),
os.path.join(state_location, "fancy_state.pkl"),
)
# Use a non-standard filename, to exercise that machinery.
app = TasksApplication(
state_location=state_location,
state_filename="fancy_state.pkl",
)
app.on_trait_change(app.exit, "application_initialized")
app.run()
state = app._state
self.assertEqual(state.previous_window_layouts[0].size, (492, 743))
[docs] def test_gui_trait_expects_IGUI_interface(self):
# Trivial test where we simply set the trait
# and the test passes because no errors are raised.
app = TasksApplication()
app.gui = DummyGUI()
[docs] @skip_with_flaky_pyside
def test_simple_lifecycle(self):
app = TasksApplication(state_location=self.tmpdir)
app.observe(lambda event: app.exit(), "application_initialized")
app.run()
[docs] @skip_with_flaky_pyside
def test_lifecycle_with_plugin(self):
events = []
plugin = LifecycleRecordingPlugin(record_to=events)
plugin.observe(events.append, "started,stopped")
gui = LifecycleRecordingGUI()
gui.observe(events.append, "starting,stopped")
app = TasksApplication(
gui=gui, state_location=self.tmpdir, plugins=[plugin]
)
app.observe(events.append, "starting,started,stopping,stopped")
# When we start and stop the application
app.observe(lambda event: app.exit(), "application_initialized")
app.run()
# Then events occurred in the following order.
self.assertEqual(
[(event.object, event.name) for event in events],
[
(app, "starting"),
(plugin, "started"),
(app, "started"),
(gui, "starting"),
(gui, "stopped"),
(app, "stopping"),
(plugin, "stopped"),
(app, "stopped"),
],
)