diff options
Diffstat (limited to '')
-rw-r--r-- | plugins/eos-updater/tests/eos_updater.py | 414 | ||||
-rwxr-xr-x | plugins/eos-updater/tests/manual-test.py | 434 |
2 files changed, 848 insertions, 0 deletions
diff --git a/plugins/eos-updater/tests/eos_updater.py b/plugins/eos-updater/tests/eos_updater.py new file mode 100644 index 0000000..5e4aa8d --- /dev/null +++ b/plugins/eos-updater/tests/eos_updater.py @@ -0,0 +1,414 @@ +'''eos-updater mock template + +This creates a mock eos-updater interface (com.endlessm.Updater), with several +methods on the Mock sidecar interface which allow its internal state flow to be +controlled. + +A typical call chain for this would be: + - Test harness calls SetPollAction('update', {}, '', '') + - SUT calls Poll() + - Test harness calls FinishPoll() + - SUT calls Fetch() + - Test harness calls FinishFetch() + - SUT calls Apply() + - Test harness calls FinishApply() + +Errors can be simulated by specifying an `early-error` or `late-error` as the +action in a Set*Action() call. `early-error` will result in the associated +Poll() call (for example) transitioning to the error state. `late-error` will +result in a transition to the error state only once (for example) FinishPoll() +is called. + +See the implementation of each Set*Action() method for the set of actions it +supports. + +Usage: + python3 -m dbusmock \ + --template ./plugins/eos-updater/tests/mock-eos-updater.py +''' + +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 of the License, or (at your option) +# any later version. See http://www.gnu.org/copyleft/lgpl.html for the full +# text of the license. +# +# The LGPL 2.1+ has been chosen as that’s the license eos-updater is under. + +from enum import IntEnum +from gi.repository import GLib +import time + +import dbus +import dbus.mainloop.glib +from dbusmock import MOCK_IFACE + + +__author__ = 'Philip Withnall' +__email__ = 'withnall@endlessm.com' +__copyright__ = '© 2019 Endless Mobile Inc.' +__license__ = 'LGPL 2.1+' + + +class UpdaterState(IntEnum): + NONE = 0 + READY = 1 + ERROR = 2 + POLLING = 3 + UPDATE_AVAILABLE = 4 + FETCHING = 5 + UPDATE_READY = 6 + APPLYING_UPDATE = 7 + UPDATE_APPLIED = 8 + + +BUS_NAME = 'com.endlessm.Updater' +MAIN_OBJ = '/com/endlessm/Updater' +MAIN_IFACE = 'com.endlessm.Updater' +SYSTEM_BUS = True + + +dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) + + +def load(mock, parameters): + mock.AddProperties( + MAIN_IFACE, + dbus.Dictionary({ + 'State': dbus.UInt32(parameters.get('State', 1)), + 'UpdateID': dbus.String(parameters.get('UpdateID', '')), + 'UpdateRefspec': dbus.String(parameters.get('UpdateRefspec', '')), + 'OriginalRefspec': + dbus.String(parameters.get('OriginalRefspec', '')), + 'CurrentID': dbus.String(parameters.get('CurrentID', '')), + 'UpdateLabel': dbus.String(parameters.get('UpdateLabel', '')), + 'UpdateMessage': dbus.String(parameters.get('UpdateMessage', '')), + 'Version': dbus.String(parameters.get('Version', '')), + 'DownloadSize': dbus.Int64(parameters.get('DownloadSize', 0)), + 'DownloadedBytes': + dbus.Int64(parameters.get('DownloadedBytes', 0)), + 'UnpackedSize': dbus.Int64(parameters.get('UnpackedSize', 0)), + 'FullDownloadSize': + dbus.Int64(parameters.get('FullDownloadSize', 0)), + 'FullUnpackedSize': + dbus.Int64(parameters.get('FullUnpackedSize', 0)), + 'ErrorCode': dbus.UInt32(parameters.get('ErrorCode', 0)), + 'ErrorName': dbus.String(parameters.get('ErrorName', '')), + 'ErrorMessage': dbus.String(parameters.get('ErrorMessage', '')), + }, signature='sv')) + + # Set up initial state + mock.__poll_action = 'no-update' + mock.__fetch_action = 'success' + mock.__apply_action = 'success' + + # Set up private methods + mock.__set_properties = __set_properties + mock.__change_state = __change_state + mock.__set_error = __set_error + mock.__check_state = __check_state + + +# +# Internal utility methods +# + +# Values in @properties must have variant_level≥1 +def __set_properties(self, iface, properties): + for key, value in properties.items(): + self.props[iface][key] = value + self.EmitSignal(dbus.PROPERTIES_IFACE, 'PropertiesChanged', 'sa{sv}as', [ + iface, + properties, + [], + ]) + + +def __change_state(self, new_state): + props = { + 'State': dbus.UInt32(new_state, variant_level=1) + } + + # Reset error state if necessary. + if new_state != UpdaterState.ERROR and \ + self.props[MAIN_IFACE]['ErrorName'] != '': + props['ErrorCode'] = dbus.UInt32(0, variant_level=1) + props['ErrorName'] = dbus.String('', variant_level=1) + props['ErrorMessage'] = dbus.String('', variant_level=1) + + self.__set_properties(self, MAIN_IFACE, props) + self.EmitSignal(MAIN_IFACE, 'StateChanged', 'u', [dbus.UInt32(new_state)]) + + +def __set_error(self, error_name, error_message): + assert(error_name != '') + + self.__set_properties(self, MAIN_IFACE, { + 'ErrorName': dbus.String(error_name, variant_level=1), + 'ErrorMessage': dbus.String(error_message, variant_level=1), + 'ErrorCode': dbus.UInt32(1, variant_level=1), + }) + self.__change_state(self, UpdaterState.ERROR) + + +def __check_state(self, allowed_states): + if self.props[MAIN_IFACE]['State'] not in allowed_states: + raise dbus.exceptions.DBusException( + 'Call not allowed in this state', + name='com.endlessm.Updater.Error.WrongState') + + +# +# Updater methods which are too big for squeezing into AddMethod() +# + +@dbus.service.method(MAIN_IFACE, in_signature='', out_signature='') +def Poll(self): + self.__check_state(self, set([ + UpdaterState.READY, + UpdaterState.UPDATE_AVAILABLE, + UpdaterState.UPDATE_READY, + UpdaterState.ERROR, + ])) + + self.__change_state(self, UpdaterState.POLLING) + + if self.__poll_action == 'early-error': + time.sleep(0.5) + self.__set_error(self, self.__poll_error_name, + self.__poll_error_message) + else: + # we now expect the test harness to call FinishPoll() on the mock + # interface + pass + + +@dbus.service.method(MAIN_IFACE, in_signature='s', out_signature='') +def PollVolume(self, path): + # FIXME: Currently unsupported + return self.Poll() + + +@dbus.service.method(MAIN_IFACE, in_signature='', out_signature='') +def Fetch(self): + return self.FetchFull() + + +@dbus.service.method(MAIN_IFACE, in_signature='a{sv}', out_signature='') +def FetchFull(self, options=None): + self.__check_state(self, set([UpdaterState.UPDATE_AVAILABLE])) + + self.__change_state(self, UpdaterState.FETCHING) + + if self.__fetch_action == 'early-error': + time.sleep(0.5) + self.__set_error(self, self.__fetch_error_name, + self.__fetch_error_message) + else: + # we now expect the test harness to call FinishFetch() on the mock + # interface + pass + + +@dbus.service.method(MAIN_IFACE, in_signature='', out_signature='') +def Apply(self): + self.__check_state(self, set([UpdaterState.UPDATE_READY])) + + self.__change_state(self, UpdaterState.APPLYING_UPDATE) + + if self.__apply_action == 'early-error': + time.sleep(0.5) + self.__set_error(self, self.__apply_error_name, + self.__apply_error_message) + else: + # we now expect the test harness to call FinishApply() on the mock + # interface + pass + + +@dbus.service.method(MAIN_IFACE, in_signature='', out_signature='') +def Cancel(self): + self.__check_state(self, set([ + UpdaterState.POLLING, + UpdaterState.FETCHING, + UpdaterState.APPLYING_UPDATE, + ])) + + time.sleep(1) + self.__set_error(self, 'com.endlessm.Updater.Error.Cancelled', + 'Update was cancelled') + + +# +# Convenience methods on the mock +# + +@dbus.service.method(MOCK_IFACE, in_signature='sa{sv}ss', out_signature='') +def SetPollAction(self, action, update_properties, error_name, error_message): + '''Set the action to happen when the SUT calls Poll(). + + This sets the action which will happen when Poll() (and subsequently + FinishPoll()) are called, including the details of the error which will be + returned or the new update which will be advertised. + ''' + # Provide a default update. + if not update_properties: + update_properties = { + 'UpdateID': dbus.String('f' * 64, variant_level=1), + 'UpdateRefspec': + dbus.String('remote:new-refspec', variant_level=1), + 'OriginalRefspec': + dbus.String('remote:old-refspec', variant_level=1), + 'CurrentID': dbus.String('1' * 64, variant_level=1), + 'UpdateLabel': dbus.String('New OS Update', variant_level=1), + 'UpdateMessage': + dbus.String('Some release notes.', variant_level=1), + 'Version': dbus.String('3.7.0', variant_level=1), + 'DownloadSize': dbus.Int64(1000000000, variant_level=1), + 'UnpackedSize': dbus.Int64(1500000000, variant_level=1), + 'FullDownloadSize': dbus.Int64(1000000000 * 0.8, variant_level=1), + 'FullUnpackedSize': dbus.Int64(1500000000 * 0.8, variant_level=1), + } + + self.__poll_action = action + self.__poll_update_properties = update_properties + self.__poll_error_name = error_name + self.__poll_error_message = error_message + + +@dbus.service.method(MOCK_IFACE, in_signature='', out_signature='') +def FinishPoll(self): + self.__check_state(self, set([UpdaterState.POLLING])) + + if self.__poll_action == 'no-update': + self.__change_state(self, UpdaterState.READY) + elif self.__poll_action == 'update': + assert(set([ + 'UpdateID', + 'UpdateRefspec', + 'OriginalRefspec', + 'CurrentID', + 'UpdateLabel', + 'UpdateMessage', + 'Version', + 'FullDownloadSize', + 'FullUnpackedSize', + 'DownloadSize', + 'UnpackedSize', + ]) <= set(self.__poll_update_properties.keys())) + + # Set the initial DownloadedBytes based on whether we know the full + # download size. + props = self.__poll_update_properties + if props['DownloadSize'] < 0: + props['DownloadedBytes'] = dbus.Int64(-1, variant_level=1) + else: + props['DownloadedBytes'] = dbus.Int64(0, variant_level=1) + + self.__set_properties(self, MAIN_IFACE, props) + self.__change_state(self, UpdaterState.UPDATE_AVAILABLE) + elif self.__poll_action == 'early-error': + # Handled in Poll() itself. + pass + elif self.__poll_action == 'late-error': + self.__set_error(self, self.__poll_error_name, + self.__poll_error_message) + else: + assert(False) + + +@dbus.service.method(MOCK_IFACE, in_signature='sss', out_signature='') +def SetFetchAction(self, action, error_name, error_message): + '''Set the action to happen when the SUT calls Fetch(). + + This sets the action which will happen when Fetch() (and subsequently + FinishFetch()) are called, including the details of the error which will be + returned, if applicable. + ''' + self.__fetch_action = action + self.__fetch_error_name = error_name + self.__fetch_error_message = error_message + + +@dbus.service.method(MOCK_IFACE, in_signature='', out_signature='', + async_callbacks=('success_cb', 'error_cb')) +def FinishFetch(self, success_cb, error_cb): + '''Finish a pending client call to Fetch(). + + This is implemented using async_callbacks since if the fetch action is + ‘success’ it will block until the simulated download is complete, emitting + download progress signals throughout. As it’s implemented asynchronously, + this allows any calls to Cancel() to be handled by the mock service + part-way through the fetch. + ''' + self.__check_state(self, set([UpdaterState.FETCHING])) + + if self.__fetch_action == 'success': + # Simulate the download. + i = 0 + download_size = self.props[MAIN_IFACE]['DownloadSize'] + + def _download_progress_cb(): + nonlocal i + + # Allow cancellation. + if self.props[MAIN_IFACE]['State'] != UpdaterState.FETCHING: + return False + + downloaded_bytes = (i / 100.0) * download_size + self.__set_properties(self, MAIN_IFACE, { + 'DownloadedBytes': + dbus.Int64(downloaded_bytes, variant_level=1), + }) + + i += 1 + + # Keep looping until the download is complete. + if i <= 100: + return True + + # When the download is complete, change the service state and + # finish the asynchronous FinishFetch() call. + self.__change_state(self, UpdaterState.UPDATE_READY) + success_cb() + return False + + GLib.timeout_add(100, _download_progress_cb) + elif self.__fetch_action == 'early-error': + # Handled in Fetch() itself. + success_cb() + elif self.__fetch_action == 'late-error': + self.__set_error(self, self.__fetch_error_name, + self.__fetch_error_message) + success_cb() + else: + assert(False) + + +@dbus.service.method(MOCK_IFACE, in_signature='sss', out_signature='') +def SetApplyAction(self, action, error_name, error_message): + '''Set the action to happen when the SUT calls Apply(). + + This sets the action which will happen when Apply() (and subsequently + FinishApply()) are called, including the details of the error which will be + returned, if applicable. + ''' + self.__apply_action = action + self.__apply_error_name = error_name + self.__apply_error_message = error_message + + +@dbus.service.method(MOCK_IFACE, in_signature='', out_signature='') +def FinishApply(self): + self.__check_state(self, set([UpdaterState.APPLYING_UPDATE])) + + if self.__apply_action == 'success': + self.__change_state(self, UpdaterState.UPDATE_APPLIED) + elif self.__apply_action == 'early-error': + # Handled in Apply() itself. + pass + elif self.__apply_action == 'late-error': + self.__set_error(self, self.__apply_error_name, + self.__apply_error_message) + else: + assert(False) diff --git a/plugins/eos-updater/tests/manual-test.py b/plugins/eos-updater/tests/manual-test.py new file mode 100755 index 0000000..b6413d9 --- /dev/null +++ b/plugins/eos-updater/tests/manual-test.py @@ -0,0 +1,434 @@ +#!/usr/bin/python3 + +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1+ of the License, or (at your option) +# any later version. See http://www.gnu.org/copyleft/lgpl.html for the full +# text of the license. +# +# The LGPL 2.1+ has been chosen as that’s the license eos-updater is under. + + +from enum import IntEnum +import os +import time +import unittest +import dbus +import dbusmock +import ddt + + +__author__ = 'Philip Withnall' +__email__ = 'withnall@endlessm.com' +__copyright__ = '© 2019 Endless Mobile Inc.' +__license__ = 'LGPL 2.1+' + + +class UpdaterState(IntEnum): + '''eos-updater states; see its State property''' + NONE = 0 + READY = 1 + ERROR = 2 + POLLING = 3 + UPDATE_AVAILABLE = 4 + FETCHING = 5 + UPDATE_READY = 6 + APPLYING_UPDATE = 7 + UPDATE_APPLIED = 8 + + +@ddt.ddt +class ManualTest(dbusmock.DBusTestCase): + '''A manual test of the eos-updater plugin in gnome-software. + + It creates a mock eos-updater D-Bus daemon, on the real system bus (because + otherwise gnome-software’s other plugins can’t communicate with their + system daemons; to fix this, we’d need to mock those up too). The test + harness provides the user with instructions about how to run gnome-software + and what to do in it, waiting for them to press enter between steps. + + FIXME: This test could potentially eventually be automated by doing the UI + steps using Dogtail or OpenQA. + + It tests various classes of interaction between the plugin and the daemon: + normal update process (with and without an update available); error returns + from the daemon; cancellation of the daemon by another process; + cancellation of the daemon from gnome-software; and the daemon unexpectedly + going away (i.e. crashing). + ''' + + @classmethod + def setUpClass(cls): + # FIXME: See the comment below about why we currently run on the actual + # system bus. + # cls.start_system_bus() + cls.dbus_con = cls.get_dbus(True) + + def setUp(self): + # Work out the path to the dbusmock template in the same directory as + # this file. + self_path = os.path.dirname(os.path.realpath(__file__)) + template_path = os.path.join(self_path, 'eos_updater.py') + + # Spawn a python-dbusmock server. Use the actual system bus, since + # gnome-software needs to access various other services (such as + # packagekit) which we don’t currently mock (FIXME). + (self.p_mock, self.obj_eos_updater) = self.spawn_server_template( + template_path, {}, stdout=None) + self.dbusmock = dbus.Interface(self.obj_eos_updater, + dbusmock.MOCK_IFACE) + + def tearDown(self): + self.kill_gnome_software() + self.p_mock.terminate() + self.p_mock.wait() + + def launch_gnome_software(self): + '''Instruct the user to launch gnome-software''' + print('Launch gnome-software with:') + print('gnome-software --verbose') + self.manual_check('Press enter to continue') + + def kill_gnome_software(self): + '''Instruct the user to kill gnome-software''' + print('Kill gnome-software with:') + print('pkill gnome-software') + self.manual_check('Press enter to continue') + + def await_state(self, state): + '''Block until eos-updater reaches the given `state`''' + print('Awaiting state %u' % state) + props_iface = dbus.Interface(self.obj_eos_updater, + dbus.PROPERTIES_IFACE) + while props_iface.Get('com.endlessm.Updater', 'State') != state: + time.sleep(0.2) + + def manual_check(self, prompt): + '''Instruct the user to do a manual check and block until done''' + input('\033[92;1m' + prompt + '\033[0m\n') + + def test_poll_no_update(self): + '''Test that no updates are shown if eos-updater successfully says + there are none.''' + self.dbusmock.SetPollAction( + 'no-update', dbus.Dictionary({}, signature='sv'), '', '') + + self.launch_gnome_software() + self.await_state(UpdaterState.POLLING) + self.dbusmock.FinishPoll() + + self.manual_check('Check there are no EOS updates listed') + self.await_state(UpdaterState.READY) + + @ddt.data('com.endlessm.Updater.Error.WrongState', + 'com.endlessm.Updater.Error.LiveBoot', + 'com.endlessm.Updater.Error.WrongConfiguration', + 'com.endlessm.Updater.Error.NotOstreeSystem', + 'com.endlessm.Updater.Error.Cancelled') + def test_poll_early_error(self, error_name): + '''Test that a D-Bus error return from Poll() is handled correctly.''' + self.dbusmock.SetPollAction( + 'early-error', dbus.Dictionary({}, signature='sv'), + error_name, 'Some error message.') + + self.launch_gnome_software() + self.await_state(UpdaterState.ERROR) + + if error_name != 'com.endlessm.Updater.Error.Cancelled': + self.manual_check('Check there are no EOS updates listed, and a ' + 'GsPluginEosUpdater error is printed on the ' + 'terminal') + else: + self.manual_check('Check there are no EOS updates listed, and no ' + 'GsPluginEosUpdater cancellation error is ' + 'printed on the terminal') + + @ddt.data('com.endlessm.Updater.Error.WrongState', + 'com.endlessm.Updater.Error.LiveBoot', + 'com.endlessm.Updater.Error.WrongConfiguration', + 'com.endlessm.Updater.Error.NotOstreeSystem', + 'com.endlessm.Updater.Error.Cancelled') + def test_poll_late_error(self, error_name): + '''Test that a transition to the Error state after successfully calling + Poll() is handled correctly.''' + self.dbusmock.SetPollAction( + 'late-error', dbus.Dictionary({}, signature='sv'), + error_name, 'Some error message.') + + self.launch_gnome_software() + self.await_state(UpdaterState.POLLING) + self.dbusmock.FinishPoll() + + if error_name != 'com.endlessm.Updater.Error.Cancelled': + self.manual_check('Check there are no EOS updates listed, and a ' + 'GsPluginEosUpdater error is printed on the ' + 'terminal') + else: + self.manual_check('Check there are no EOS updates listed, and no ' + 'GsPluginEosUpdater cancellation error is ' + 'printed on the terminal') + self.await_state(UpdaterState.ERROR) + + @ddt.data(True, False) + def test_update_available(self, manually_refresh): + '''Test that the entire update process works if an update is + available.''' + self.dbusmock.SetPollAction( + 'update', dbus.Dictionary({}, signature='sv'), '', '') + self.dbusmock.SetFetchAction('success', '', '') + self.dbusmock.SetApplyAction('success', '', '') + + self.launch_gnome_software() + self.await_state(UpdaterState.POLLING) + self.dbusmock.FinishPoll() + + if manually_refresh: + self.manual_check('Check an EOS update is listed; press the ' + 'Refresh button') + + # TODO: if you proceed through the test slowly, this sometimes doesn’t + # work + self.manual_check('Check an EOS update is listed; press the Download ' + 'button') + self.await_state(UpdaterState.FETCHING) + self.dbusmock.FinishFetch() + + self.manual_check('Check the download has paused at ~75% complete ' + '(waiting to apply)') + self.await_state(UpdaterState.APPLYING_UPDATE) + self.dbusmock.FinishApply() + + self.manual_check('Check the banner says to ‘Restart Now’ (don’t ' + 'click it)') + self.await_state(UpdaterState.UPDATE_APPLIED) + + @ddt.data('com.endlessm.Updater.Error.WrongState', + 'com.endlessm.Updater.Error.WrongConfiguration', + 'com.endlessm.Updater.Error.Fetching', + 'com.endlessm.Updater.Error.MalformedAutoinstallSpec', + 'com.endlessm.Updater.Error.UnknownEntryInAutoinstallSpec', + 'com.endlessm.Updater.Error.FlatpakRemoteConflict', + 'com.endlessm.Updater.Error.MeteredConnection', + 'com.endlessm.Updater.Error.Cancelled') + def test_fetch_early_error(self, error_name): + '''Test that a D-Bus error return from Fetch() is handled correctly.''' + self.dbusmock.SetPollAction( + 'update', dbus.Dictionary({}, signature='sv'), '', '') + self.dbusmock.SetFetchAction('early-error', error_name, + 'Some error or other.') + + self.launch_gnome_software() + self.await_state(UpdaterState.POLLING) + self.dbusmock.FinishPoll() + + self.manual_check('Check an EOS update is listed; press the Download ' + 'button') + + if error_name != 'com.endlessm.Updater.Error.Cancelled': + self.manual_check('Check a fetch error is displayed') + else: + self.manual_check('Check no cancellation error is displayed') + + self.await_state(UpdaterState.POLLING) + self.dbusmock.FinishPoll() + self.manual_check('Check an EOS update is listed again') + + @ddt.data('com.endlessm.Updater.Error.WrongState', + 'com.endlessm.Updater.Error.WrongConfiguration', + 'com.endlessm.Updater.Error.Fetching', + 'com.endlessm.Updater.Error.MalformedAutoinstallSpec', + 'com.endlessm.Updater.Error.UnknownEntryInAutoinstallSpec', + 'com.endlessm.Updater.Error.FlatpakRemoteConflict', + 'com.endlessm.Updater.Error.MeteredConnection', + 'com.endlessm.Updater.Error.Cancelled') + def test_fetch_late_error(self, error_name): + '''Test that a transition to the Error state after successfully calling + Fetch() is handled correctly.''' + self.dbusmock.SetPollAction( + 'update', dbus.Dictionary({}, signature='sv'), '', '') + self.dbusmock.SetFetchAction('late-error', error_name, + 'Some error or other.') + + self.launch_gnome_software() + self.await_state(UpdaterState.POLLING) + self.dbusmock.FinishPoll() + + self.manual_check('Check an EOS update is listed; press the Download ' + 'button') + self.await_state(UpdaterState.FETCHING) + self.dbusmock.FinishFetch() + + self.await_state(UpdaterState.ERROR) + if error_name != 'com.endlessm.Updater.Error.Cancelled': + self.manual_check('Check a fetch error is displayed') + else: + self.manual_check('Check no cancellation error is displayed') + + self.await_state(UpdaterState.POLLING) + self.dbusmock.FinishPoll() + self.manual_check('Check an EOS update is listed again') + + @ddt.data('com.endlessm.Updater.Error.WrongState', + 'com.endlessm.Updater.Error.WrongConfiguration', + 'com.endlessm.Updater.Error.Cancelled') + def test_apply_early_error(self, error_name): + '''Test that a D-Bus error return from Apply() is handled correctly.''' + self.dbusmock.SetPollAction( + 'update', dbus.Dictionary({}, signature='sv'), '', '') + self.dbusmock.SetFetchAction('success', '', '') + self.dbusmock.SetApplyAction('early-error', error_name, + 'Some error or other.') + + self.launch_gnome_software() + self.await_state(UpdaterState.POLLING) + self.dbusmock.FinishPoll() + + self.manual_check('Check an EOS update is listed; press the Download ' + 'button') + self.await_state(UpdaterState.FETCHING) + self.dbusmock.FinishFetch() + + self.await_state(UpdaterState.ERROR) + if error_name != 'com.endlessm.Updater.Error.Cancelled': + self.manual_check('Check an apply error is displayed after the ' + 'update reached ~75% completion') + else: + self.manual_check('Check no cancellation error is displayed after ' + 'the update reached ~75% completion') + + self.await_state(UpdaterState.POLLING) + self.dbusmock.FinishPoll() + self.manual_check('Check an EOS update is listed again') + + @ddt.data('com.endlessm.Updater.Error.WrongState', + 'com.endlessm.Updater.Error.WrongConfiguration', + 'com.endlessm.Updater.Error.Cancelled') + def test_apply_late_error(self, error_name): + '''Test that a transition to the Error state after successfully calling + Apply() is handled correctly.''' + self.dbusmock.SetPollAction( + 'update', dbus.Dictionary({}, signature='sv'), '', '') + self.dbusmock.SetFetchAction('success', '', '') + self.dbusmock.SetApplyAction('late-error', error_name, + 'Some error or other.') + + self.launch_gnome_software() + self.await_state(UpdaterState.POLLING) + self.dbusmock.FinishPoll() + + self.manual_check('Check an EOS update is listed; press the Download ' + 'button') + self.await_state(UpdaterState.FETCHING) + self.dbusmock.FinishFetch() + + self.manual_check('Check the download has paused at ~75% complete ' + '(waiting to apply)') + self.await_state(UpdaterState.APPLYING_UPDATE) + self.dbusmock.FinishApply() + + self.await_state(UpdaterState.ERROR) + if error_name != 'com.endlessm.Updater.Error.Cancelled': + self.manual_check('Check an apply error is displayed') + else: + self.manual_check('Check no cancellation error is displayed') + + self.await_state(UpdaterState.POLLING) + self.dbusmock.FinishPoll() + self.manual_check('Check an EOS update is listed again') + + def test_no_eos_updater_running(self): + '''Test that the plugin doesn’t make a fuss if eos-updater is + unavailable.''' + self.p_mock.kill() + + self.launch_gnome_software() + + self.manual_check('Check there are no EOS updates listed, and no ' + 'errors shown') + + def test_fetch_ui_cancellation(self): + '''Test that cancelling a download from the UI works correctly.''' + self.dbusmock.SetPollAction( + 'update', dbus.Dictionary({}, signature='sv'), '', '') + self.dbusmock.SetFetchAction('success', '', '') + + self.launch_gnome_software() + self.await_state(UpdaterState.POLLING) + self.dbusmock.FinishPoll() + + self.manual_check('Check an EOS update is listed; press the Download ' + 'button, then shortly afterwards press the Cancel ' + 'button') + self.await_state(UpdaterState.FETCHING) + self.dbusmock.FinishFetch() + + self.await_state(UpdaterState.ERROR) + self.manual_check('Check a fetch cancellation error is displayed') + + def test_poll_eos_updater_dies(self): + '''Test that gnome-software recovers if eos-updater dies while + polling for updates.''' + self.dbusmock.SetPollAction( + 'update', dbus.Dictionary({}, signature='sv'), '', '') + + self.launch_gnome_software() + self.await_state(UpdaterState.POLLING) + self.p_mock.kill() + + self.manual_check('Check no error is shown for the poll failure') + self.setUp() + self.dbusmock.SetPollAction( + 'update', dbus.Dictionary({}, signature='sv'), '', '') + + self.manual_check('Press the Refresh button and check an update is ' + 'shown') + # TODO: It may take a few minutes for the update to appear on the + # updates page + self.await_state(UpdaterState.POLLING) + self.dbusmock.FinishPoll() + + def test_fetch_eos_updater_dies(self): + '''Test that gnome-software recovers if eos-updater dies while + fetching an update.''' + self.dbusmock.SetPollAction( + 'update', dbus.Dictionary({}, signature='sv'), '', '') + self.dbusmock.SetFetchAction('success', '', '') + + self.launch_gnome_software() + self.await_state(UpdaterState.POLLING) + self.dbusmock.FinishPoll() + + self.manual_check('Check an EOS update is listed; press the Download ' + 'button') + self.await_state(UpdaterState.FETCHING) + self.p_mock.kill() + + self.manual_check('Check an error is shown for the fetch failure') + + def test_apply_eos_updater_dies(self): + '''Test that gnome-software recovers if eos-updater dies while + applying an update.''' + self.dbusmock.SetPollAction( + 'update', dbus.Dictionary({}, signature='sv'), '', '') + self.dbusmock.SetFetchAction('success', '', '') + self.dbusmock.SetApplyAction('success', '', '') + + self.launch_gnome_software() + self.await_state(UpdaterState.POLLING) + self.dbusmock.FinishPoll() + + self.manual_check('Check an EOS update is listed; press the Download ' + 'button') + self.await_state(UpdaterState.FETCHING) + self.dbusmock.FinishFetch() + + self.manual_check('Check the download has paused at ~75% complete ' + '(waiting to apply)') + self.await_state(UpdaterState.APPLYING_UPDATE) + self.p_mock.kill() + + self.manual_check('Check an error is shown for the apply failure') + + +if __name__ == '__main__': + unittest.main() |