#!/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()