def setUp(self, is_fallback=False): super(UpdateTestCase, self).setUp() self.software_update = SoftwareUpdate(self.marionette) # If requested modify the list of allowed MAR channels if self.update_mar_channels: self.software_update.mar_channels.add_channels(self.update_mar_channels) # Ensure that there exists no already partially downloaded update self.remove_downloaded_update() self.set_preferences_defaults() # Dictionary which holds the information for each update self.updates = [{ 'build_pre': self.software_update.build_info, 'build_post': None, 'fallback': is_fallback, 'patch': {}, 'success': False, }] self.assertTrue(self.update_mar_channels.issubset( self.software_update.mar_channels.channels), 'Allowed MAR channels have been set: expected "{}" in "{}"'.format( ', '.join(self.update_mar_channels), ', '.join(self.software_update.mar_channels.channels))) # Check if the user has permissions to run the update self.assertTrue(self.software_update.allowed, 'Current user has permissions to update the application.')
def setUp(self): FirefoxTestCase.setUp(self) self.software_update = SoftwareUpdate(lambda: self.marionette) self.saved_mar_channels = self.software_update.mar_channels.channels self.software_update.mar_channels.channels = set( ['expected', 'channels'])
class TestSoftwareUpdate(FirefoxTestCase): def setUp(self): FirefoxTestCase.setUp(self) self.software_update = SoftwareUpdate(lambda: self.marionette) self.saved_mar_channels = self.software_update.mar_channels.channels self.software_update.mar_channels.channels = set( ['expected', 'channels']) def tearDown(self): try: self.software_update.mar_channels.channels = self.saved_mar_channels finally: FirefoxTestCase.tearDown(self) def test_abi(self): self.assertTrue(self.software_update.ABI) def test_allowed(self): self.assertTrue(self.software_update.allowed) def test_build_info(self): build_info = self.software_update.build_info self.assertEqual(build_info['disabled_addons'], None) self.assertIn('Mozilla/', build_info['user_agent']) self.assertEqual(build_info['mar_channels'], set(['expected', 'channels'])) self.assertTrue(build_info['version']) self.assertTrue(build_info['buildid'].isdigit()) self.assertTrue(build_info['locale']) self.assertIn('force=1', build_info['update_url']) self.assertIn('xml', build_info['update_snippet']) self.assertEqual(build_info['channel'], self.software_update.update_channel.channel) def test_force_fallback(self): status_file = os.path.join(self.software_update.staging_directory, 'update.status') try: self.software_update.force_fallback() with open(status_file, 'r') as f: content = f.read() self.assertEqual(content, 'failed: 6\n') finally: os.remove(status_file) def test_get_update_url(self): update_url = self.software_update.get_update_url() self.assertIn('Firefox', update_url) self.assertNotIn('force=1', update_url) update_url = self.software_update.get_update_url(True) self.assertIn('Firefox', update_url) self.assertIn('force=1', update_url) def test_os_version(self): self.assertTrue(self.software_update.os_version) def test_staging_directory(self): self.assertTrue(self.software_update.staging_directory)
def setUp(self): super(TestSoftwareUpdate, self).setUp() self.software_update = SoftwareUpdate(self.marionette) self.saved_mar_channels = self.software_update.mar_channels.channels self.software_update.mar_channels.channels = set(['expected', 'channels'])
def setUp(self, is_fallback=False): FirefoxTestCase.setUp(self) self.software_update = SoftwareUpdate(lambda: self.marionette) # Bug 604364 - Preparation to test multiple update steps self.current_update_index = 0 self.staging_directory = self.software_update.staging_directory # If requested modify the default update channel. It will be active # after the next restart of the application # Bug 1142805 - Modify file via Python directly if self.update_channel: # Backup the original content and the path of the channel-prefs.js file self.default_update_channel = { 'content': self.software_update.update_channel.file_contents, 'path': self.software_update.update_channel.file_path, } self.software_update.update_channel.default_channel = self.update_channel # If requested modify the list of allowed MAR channels # Bug 1142805 - Modify file via Python directly if self.update_mar_channels: # Backup the original content and the path of the update-settings.ini file self.default_mar_channels = { 'content': self.software_update.mar_channels.config_file_contents, 'path': self.software_update.mar_channels.config_file_path, } self.software_update.mar_channels.add_channels( self.update_mar_channels) # Bug 1142805 - Until we don't modify the channel-prefs.js and update-settings.ini # files before Firefox gets started, a restart of Firefox is necessary to # accept the new update channel. self.restart() # Dictionary which holds the information for each update self.updates = [{ 'build_pre': self.software_update.build_info, 'build_post': None, 'fallback': is_fallback, 'patch': {}, 'success': False, }] self.assertEqual(self.software_update.update_channel.default_channel, self.software_update.update_channel.channel) self.assertTrue( self.update_mar_channels.issubset( self.software_update.mar_channels.channels), 'Allowed MAR channels have been set: expected "{}" in "{}"'.format( ', '.join(self.update_mar_channels), ', '.join(self.software_update.mar_channels.channels))) # Check if the user has permissions to run the update self.assertTrue( self.software_update.allowed, 'Current user has permissions to update the application.')
class TestSoftwareUpdate(PuppeteerMixin, MarionetteTestCase): def setUp(self): super(TestSoftwareUpdate, self).setUp() self.software_update = SoftwareUpdate(self.marionette) self.saved_mar_channels = self.software_update.mar_channels.channels self.software_update.mar_channels.channels = set(['expected', 'channels']) def tearDown(self): try: self.software_update.mar_channels.channels = self.saved_mar_channels finally: super(TestSoftwareUpdate, self).tearDown() def test_abi(self): self.assertTrue(self.software_update.ABI) def test_allowed(self): self.assertTrue(self.software_update.allowed) def test_build_info(self): build_info = self.software_update.build_info self.assertEqual(build_info['disabled_addons'], None) self.assertIn('Mozilla/', build_info['user_agent']) self.assertEqual(build_info['mar_channels'], set(['expected', 'channels'])) self.assertTrue(build_info['version']) self.assertTrue(build_info['buildid'].isdigit()) self.assertTrue(build_info['locale']) self.assertIn('force=1', build_info['update_url']) self.assertIn('xml', build_info['update_snippet']) self.assertEqual(build_info['channel'], self.software_update.update_channel.channel) def test_force_fallback(self): status_file = os.path.join(self.software_update.staging_directory, 'update.status') try: self.software_update.force_fallback() with open(status_file, 'r') as f: content = f.read() self.assertEqual(content, 'failed: 6\n') finally: os.remove(status_file) def test_get_update_url(self): update_url = self.software_update.get_update_url() self.assertIn('Firefox', update_url) self.assertNotIn('force=1', update_url) update_url = self.software_update.get_update_url(True) self.assertIn('Firefox', update_url) self.assertIn('force=1', update_url) def test_os_version(self): self.assertTrue(self.software_update.os_version) def test_staging_directory(self): self.assertTrue(self.software_update.staging_directory)
class TestSoftwareUpdate(FirefoxTestCase): def setUp(self): FirefoxTestCase.setUp(self) self.software_update = SoftwareUpdate(lambda: self.marionette) self.saved_mar_channels = self.software_update.mar_channels.channels self.software_update.mar_channels.channels = set(["expected", "channels"]) def tearDown(self): try: self.software_update.mar_channels.channels = self.saved_mar_channels finally: FirefoxTestCase.tearDown(self) def test_abi(self): self.assertTrue(self.software_update.ABI) def test_allowed(self): self.assertTrue(self.software_update.allowed) def test_build_info(self): build_info = self.software_update.build_info self.assertEqual(build_info["disabled_addons"], None) self.assertIn("Mozilla/", build_info["user_agent"]) self.assertEqual(build_info["mar_channels"], set(["expected", "channels"])) self.assertTrue(build_info["version"]) self.assertTrue(build_info["buildid"].isdigit()) self.assertTrue(build_info["locale"]) self.assertIn("force=1", build_info["url_aus"]) self.assertEqual(build_info["channel"], self.software_update.update_channel.channel) def test_force_fallback(self): status_file = os.path.join(self.software_update.staging_directory, "update.status") try: self.software_update.force_fallback() with open(status_file, "r") as f: content = f.read() self.assertEqual(content, "failed: 6\n") finally: os.remove(status_file) def test_get_update_url(self): update_url = self.software_update.get_update_url() self.assertIn("Firefox", update_url) self.assertNotIn("force=1", update_url) update_url = self.software_update.get_update_url(True) self.assertIn("Firefox", update_url) self.assertIn("force=1", update_url) def test_os_version(self): self.assertTrue(self.software_update.os_version) def test_staging_directory(self): self.assertTrue(self.software_update.staging_directory)
def setUp(self): super(TestUpdateChannel, self).setUp() self.software_update = SoftwareUpdate(self.marionette) self.saved_channel = self.software_update.update_channel self.software_update.update_channel = 'expected_channel'
def setUp(self, is_fallback=False): super(UpdateTestCase, self).setUp() self.software_update = SoftwareUpdate(self.marionette) self.download_duration = None # If a custom update channel has to be set, force a restart of # Firefox to actually get it applied as a default pref. Use the clean # option to force a non in_app restart, which would allow Firefox to # dump the logs to the console. if self.update_channel: self.software_update.update_channel = self.update_channel self.restart(clean=True) self.assertEqual(self.software_update.update_channel, self.update_channel) # If requested modify the list of allowed MAR channels if self.update_mar_channels: self.software_update.mar_channels.add_channels( self.update_mar_channels) self.assertTrue( self.update_mar_channels.issubset( self.software_update.mar_channels.channels), 'Allowed MAR channels have been set: expected "{}" in "{}"'. format(', '.join(self.update_mar_channels), ', '.join(self.software_update.mar_channels.channels))) # Ensure that there exists no already partially downloaded update self.remove_downloaded_update() self.set_preferences_defaults() # Dictionary which holds the information for each update self.update_status = { 'build_pre': self.software_update.build_info, 'build_post': None, 'fallback': is_fallback, 'patch': {}, 'success': False, } # Check if the user has permissions to run the update self.assertTrue( self.software_update.allowed, 'Current user has permissions to update the application.')
def setUp(self, is_fallback=False): super(UpdateTestCase, self).setUp() self.software_update = SoftwareUpdate(lambda: self.marionette) self.download_duration = None # Bug 604364 - Preparation to test multiple update steps self.current_update_index = 0 # Ensure that there exists no already partially downloaded update self.remove_downloaded_update() # If requested modify the default update channel. It will be active # after the next restart of the application # Bug 1142805 - Modify file via Python directly if self.update_channel: # Backup the original content and the path of the channel-prefs.js file self.default_update_channel = { 'content': self.software_update.update_channel.file_contents, 'path': self.software_update.update_channel.file_path, } self.software_update.update_channel.default_channel = self.update_channel # If requested modify the list of allowed MAR channels # Bug 1142805 - Modify file via Python directly if self.update_mar_channels: # Backup the original content and the path of the update-settings.ini file self.default_mar_channels = { 'content': self.software_update.mar_channels.config_file_contents, 'path': self.software_update.mar_channels.config_file_path, } self.software_update.mar_channels.add_channels(self.update_mar_channels) # Bug 1142805 - Until we don't modify the channel-prefs.js and update-settings.ini # files before Firefox gets started, a restart of Firefox is necessary to # accept the new update channel. self.restart() # Dictionary which holds the information for each update self.updates = [{ 'build_pre': self.software_update.build_info, 'build_post': None, 'fallback': is_fallback, 'patch': {}, 'success': False, }] self.assertEqual(self.software_update.update_channel.default_channel, self.software_update.update_channel.channel) self.assertTrue(self.update_mar_channels.issubset( self.software_update.mar_channels.channels), 'Allowed MAR channels have been set: expected "{}" in "{}"'.format( ', '.join(self.update_mar_channels), ', '.join(self.software_update.mar_channels.channels))) # Check if the user has permissions to run the update self.assertTrue(self.software_update.allowed, 'Current user has permissions to update the application.')
def setUp(self, is_fallback=False): FirefoxTestCase.setUp(self) self.software_update = SoftwareUpdate(lambda: self.marionette) # Bug 604364 - Preparation to test multiple update steps self.current_update_index = 0 self.staging_directory = self.software_update.staging_directory # If requested modify the default update channel. It will be active # after the next restart of the application # Bug 1142805 - Modify file via Python directly if self.update_channel: # Backup the original content and the path of the channel-prefs.js file self.default_update_channel = { 'content': self.software_update.update_channel.file_contents, 'path': self.software_update.update_channel.file_path, } self.software_update.update_channel.default_channel = self.update_channel # If requested modify the list of allowed MAR channels # Bug 1142805 - Modify file via Python directly if self.update_mar_channels: # Backup the original content and the path of the update-settings.ini file self.default_mar_channels = { 'content': self.software_update.mar_channels.config_file_contents, 'path': self.software_update.mar_channels.config_file_path, } self.software_update.mar_channels.add_channels(self.update_mar_channels) # Bug 1142805 - Until we don't modify the channel-prefs.js and update-settings.ini # files before Firefox gets started, a restart of Firefox is necessary to # accept the new update channel. self.restart() # Dictionary which holds the information for each update self.updates = [{ 'build_pre': self.software_update.build_info, 'build_post': None, 'fallback': is_fallback, 'patch': {}, 'success': False, }] self.assertEqual(self.software_update.update_channel.default_channel, self.software_update.update_channel.channel) self.assertTrue(self.update_mar_channels.issubset( self.software_update.mar_channels.channels)) # Check if the user has permissions to run the update self.assertTrue(self.software_update.allowed)
class UpdateTestCase(FirefoxTestCase): def __init__(self, *args, **kwargs): FirefoxTestCase.__init__(self, *args, **kwargs) self.target_buildid = kwargs.pop('update_target_buildid') self.target_version = kwargs.pop('update_target_version') self.update_channel = kwargs.pop('update_channel') self.default_update_channel = None self.update_mar_channels = set(kwargs.pop('update_mar_channels')) self.default_mar_channels = None self.updates = [] def setUp(self, is_fallback=False): FirefoxTestCase.setUp(self) self.software_update = SoftwareUpdate(lambda: self.marionette) # Bug 604364 - Preparation to test multiple update steps self.current_update_index = 0 self.staging_directory = self.software_update.staging_directory # If requested modify the default update channel. It will be active # after the next restart of the application # Bug 1142805 - Modify file via Python directly if self.update_channel: # Backup the original content and the path of the channel-prefs.js file self.default_update_channel = { 'content': self.software_update.update_channel.file_contents, 'path': self.software_update.update_channel.file_path, } self.software_update.update_channel.default_channel = self.update_channel # If requested modify the list of allowed MAR channels # Bug 1142805 - Modify file via Python directly if self.update_mar_channels: # Backup the original content and the path of the update-settings.ini file self.default_mar_channels = { 'content': self.software_update.mar_channels.config_file_contents, 'path': self.software_update.mar_channels.config_file_path, } self.software_update.mar_channels.add_channels(self.update_mar_channels) # Bug 1142805 - Until we don't modify the channel-prefs.js and update-settings.ini # files before Firefox gets started, a restart of Firefox is necessary to # accept the new update channel. self.restart() # Dictionary which holds the information for each update self.updates = [{ 'build_pre': self.software_update.build_info, 'build_post': None, 'fallback': is_fallback, 'patch': {}, 'success': False, }] self.assertEqual(self.software_update.update_channel.default_channel, self.software_update.update_channel.channel) self.assertTrue(self.update_mar_channels.issubset( self.software_update.mar_channels.channels)) # Check if the user has permissions to run the update self.assertTrue(self.software_update.allowed) def assert_update_applied(self, update_status): """Checks if an update has been applied correctly. :param update_status: All the data collected during the update process """ # Get the information from the last update info = update_status[self.current_update_index] # The upgraded version should be identical with the version given by # the update and we shouldn't have run a downgrade check = self.marionette.execute_script(""" Components.utils.import("resource://gre/modules/Services.jsm"); return Services.vc.compare(arguments[0], arguments[1]); """, script_args=[info['build_post']['version'], info['build_pre']['version']]) self.assertGreaterEqual(check, 0, 'The version of the upgraded build is higher or equal') # If a target version has been specified, check if it matches the updated build if self.target_version: self.assertEqual(info['build_post']['version'], self.target_version) # The post buildid should be identical with the buildid contained in the patch self.assertEqual(info['build_post']['buildid'], info['patch']['buildid']) # If a target buildid has been specified, check if it matches the updated build if self.target_buildid: self.assertEqual(info['build_post']['buildid'], self.target_buildid) # An upgrade should not change the builds locale self.assertEqual(info['build_post']['locale'], info['build_pre']['locale']) # Check that no application-wide add-ons have been disabled self.assertEqual(info['build_post']['disabled_addons'], info['build_pre']['disabled_addons']) def check_update_applied(self): self.updates[self.current_update_index]['build_post'] = self.software_update.build_info about_window = self.browser.open_about_window() try: update_available = about_window.check_for_updates() # No further updates should be offered now with the same update type if update_available: about_window.download(wait_for_finish=False) self.assertNotEqual(self.software_update.active_update.type, self.updates[self.current_update_index].type) # Check that updates have been applied correctly self.assert_update_applied(self.updates) self.updates[self.current_update_index]['success'] = True finally: about_window.close() def download_and_apply_available_update(self, force_fallback=False): """Checks, downloads, and applies an available update. :param force_fallback: Optional, if `True` invalidate current update status. Defaults to `False`. """ # Open the about window and check for updates about_window = self.browser.open_about_window() try: update_available = about_window.check_for_updates() self.assertTrue(update_available) # Download update and wait until it has been applied about_window.download() about_window.wait_for_update_applied() finally: self.updates[self.current_update_index]['patch'] = about_window.patch_info if force_fallback: # Set the downloaded update into failed state self.software_update.force_fallback() # Restart Firefox to apply the downloaded update self.restart() def download_and_apply_forced_update(self): # The update wizard dialog opens automatically after the restart dialog = self.windows.switch_to(lambda win: type(win) is UpdateWizardDialog) # In case of a broken complete update the about window has to be used if self.updates[self.current_update_index]['patch']['is_complete']: try: self.assertEqual(dialog.wizard.selected_panel, dialog.wizard.error) dialog.close() # Open the about window and check for updates about_window = self.browser.open_about_window() update_available = about_window.check_for_updates() self.assertTrue(update_available) # Download update and wait until it has been applied about_window.download() about_window.wait_for_update_applied() finally: self.updates[self.current_update_index]['patch'] = about_window.patch_info else: try: self.assertEqual(dialog.wizard.selected_panel, dialog.wizard.error_patching) # Start downloading the fallback update dialog.download() dialog.close() finally: self.updates[self.current_update_index]['patch'] = dialog.patch_info # Restart Firefox to apply the update self.restart() def restore_config_files(self): # Reset channel-prefs.js file if modified try: if self.default_update_channel: path = self.default_update_channel['path'] self.logger.info('Restoring channel defaults for: {}'.format(path)) with open(path, 'w') as f: f.write(self.default_update_channel['content']) except IOError: self.logger.error('Failed to reset the default update channel.', exc_info=True) # Reset update-settings.ini file if modified try: if self.default_mar_channels: path = self.default_mar_channels['path'] self.logger.info('Restoring mar channel defaults for: {}'.format(path)) with open(path, 'w') as f: f.write(self.default_mar_channels['content']) except IOError: self.logger.error('Failed to reset the default mar channels.', exc_info=True) def tearDown(self): try: self.browser.tabbar.close_all_tabs([self.browser.tabbar.selected_tab]) # Print results for now until we have treeherder integration output = pprint.pformat(self.updates) self.logger.info('Update test results: \n{}'.format(output)) finally: FirefoxTestCase.tearDown(self) self.restore_config_files()
class TestSoftwareUpdate(PuppeteerMixin, MarionetteTestCase): def setUp(self): super(TestSoftwareUpdate, self).setUp() self.software_update = SoftwareUpdate(self.marionette) self.saved_mar_channels = self.software_update.mar_channels.channels self.software_update.mar_channels.channels = set( ['expected', 'channels']) def tearDown(self): try: self.software_update.mar_channels.channels = self.saved_mar_channels finally: super(TestSoftwareUpdate, self).tearDown() def test_abi(self): self.assertTrue(self.software_update.ABI) def test_allowed(self): self.assertTrue(self.software_update.allowed) def test_build_info(self): self.software_update.update_url = self.marionette.absolute_url( 'update/snippet_empty.xml?product=%PRODUCT%&version=%VERSION%&' 'buildid=%BUILD_ID%&locale=%LOCALE%&channel=%CHANNEL%') try: build_info = self.software_update.build_info self.assertEqual(build_info['disabled_addons'], None) self.assertIn('Mozilla/', build_info['user_agent']) self.assertEqual(build_info['mar_channels'], set(['expected', 'channels'])) self.assertTrue(build_info['version']) self.assertTrue(build_info['buildid'].isdigit()) self.assertTrue(build_info['locale']) self.assertIn('force=1', build_info['update_url']) self.assertIn('xml', build_info['update_snippet']) self.assertEqual(build_info['channel'], self.software_update.update_channel) finally: # Restart Firefox to reset the custom update url self.restart(clean=True) def test_force_fallback(self): status_file = os.path.join(self.software_update.staging_directory, 'update.status') try: self.software_update.force_fallback() with open(status_file, 'r') as f: content = f.read() self.assertEqual(content, 'failed: 6\n') finally: os.remove(status_file) def test_get_update_url(self): update_url = self.software_update.get_formatted_update_url() self.assertIn('Firefox', update_url) self.assertNotIn('force=1', update_url) update_url = self.software_update.get_formatted_update_url(True) self.assertIn('Firefox', update_url) self.assertIn('force=1', update_url) def test_os_version(self): self.assertTrue(self.software_update.os_version) def test_staging_directory(self): self.assertTrue(self.software_update.staging_directory)
def setUp(self): FirefoxTestCase.setUp(self) self.software_update = SoftwareUpdate(lambda: self.marionette) self.saved_channel = self.software_update.update_channel.default_channel self.software_update.update_channel.default_channel = 'expected_channel'
def __init__(self, *args, **kwargs): BaseWindow.__init__(self, *args, **kwargs) self._software_update = SoftwareUpdate(lambda: self.marionette) self._download_duration = None
class UpdateTestCase(FirefoxTestCase): TIMEOUT_UPDATE_APPLY = 300 TIMEOUT_UPDATE_CHECK = 30 TIMEOUT_UPDATE_DOWNLOAD = 360 # For the old update wizard, the errors are displayed inside the dialog. For the # handling of updates in the about window the errors are displayed in new dialogs. # When the old wizard is open we have to set the preference, so the errors will be # shown as expected, otherwise we would have unhandled modal dialogs when errors are # raised. See: # http://mxr.mozilla.org/mozilla-central/source/toolkit/mozapps/update/nsUpdateService.js?rev=a9240b1eb2fb#4813 # http://mxr.mozilla.org/mozilla-central/source/toolkit/mozapps/update/nsUpdateService.js?rev=a9240b1eb2fb#4756 PREF_APP_UPDATE_ALTWINDOWTYPE = 'app.update.altwindowtype' def __init__(self, *args, **kwargs): super(UpdateTestCase, self).__init__(*args, **kwargs) self.target_buildid = kwargs.pop('update_target_buildid') self.target_version = kwargs.pop('update_target_version') self.update_channel = kwargs.pop('update_channel') self.default_update_channel = None self.update_mar_channels = set(kwargs.pop('update_mar_channels')) self.default_mar_channels = None self.updates = [] def setUp(self, is_fallback=False): super(UpdateTestCase, self).setUp() self.software_update = SoftwareUpdate(lambda: self.marionette) self.download_duration = None # Bug 604364 - Preparation to test multiple update steps self.current_update_index = 0 # Ensure that there exists no already partially downloaded update self.remove_downloaded_update() # If requested modify the default update channel. It will be active # after the next restart of the application # Bug 1142805 - Modify file via Python directly if self.update_channel: # Backup the original content and the path of the channel-prefs.js file self.default_update_channel = { 'content': self.software_update.update_channel.file_contents, 'path': self.software_update.update_channel.file_path, } self.software_update.update_channel.default_channel = self.update_channel # If requested modify the list of allowed MAR channels # Bug 1142805 - Modify file via Python directly if self.update_mar_channels: # Backup the original content and the path of the update-settings.ini file self.default_mar_channels = { 'content': self.software_update.mar_channels.config_file_contents, 'path': self.software_update.mar_channels.config_file_path, } self.software_update.mar_channels.add_channels(self.update_mar_channels) # Bug 1142805 - Until we don't modify the channel-prefs.js and update-settings.ini # files before Firefox gets started, a restart of Firefox is necessary to # accept the new update channel. self.restart() # Dictionary which holds the information for each update self.updates = [{ 'build_pre': self.software_update.build_info, 'build_post': None, 'fallback': is_fallback, 'patch': {}, 'success': False, }] self.assertEqual(self.software_update.update_channel.default_channel, self.software_update.update_channel.channel) self.assertTrue(self.update_mar_channels.issubset( self.software_update.mar_channels.channels), 'Allowed MAR channels have been set: expected "{}" in "{}"'.format( ', '.join(self.update_mar_channels), ', '.join(self.software_update.mar_channels.channels))) # Check if the user has permissions to run the update self.assertTrue(self.software_update.allowed, 'Current user has permissions to update the application.') def tearDown(self): try: self.browser.tabbar.close_all_tabs([self.browser.tabbar.selected_tab]) # Add content of the update log file for detailed failures when applying an update self.updates[self.current_update_index]['update_log'] = self.read_update_log() # Print results for now until we have treeherder integration output = pprint.pformat(self.updates) self.logger.info('Update test results: \n{}'.format(output)) finally: super(UpdateTestCase, self).tearDown() # Ensure that no trace of an partially downloaded update remain self.remove_downloaded_update() self.restore_config_files() @property def patch_info(self): """ Returns information about the active update in the queue. :returns: A dictionary with information about the active patch """ patch = self.software_update.patch_info patch['download_duration'] = self.download_duration return patch def check_for_updates(self, about_window, timeout=TIMEOUT_UPDATE_CHECK): """Clicks on "Check for Updates" button, and waits for check to complete. :param about_window: Instance of :class:`AboutWindow`. :param timeout: How long to wait for the update check to finish. Optional, defaults to 60s. :returns: True, if an update is available. """ self.assertEqual(about_window.deck.selected_panel, about_window.deck.check_for_updates) about_window.deck.check_for_updates.button.click() Wait(self.marionette, timeout=self.TIMEOUT_UPDATE_CHECK).until( lambda _: about_window.deck.selected_panel not in (about_window.deck.check_for_updates, about_window.deck.checking_for_updates), message='Check for updates has been finished.') return about_window.deck.selected_panel != about_window.deck.no_updates_found def check_update_applied(self): """Check that the update has been applied correctly""" update = self.updates[self.current_update_index] update['build_post'] = self.software_update.build_info about_window = self.browser.open_about_window() try: update_available = self.check_for_updates(about_window) # The upgraded version should be identical with the version given by # the update and we shouldn't have run a downgrade check = self.marionette.execute_script(""" Components.utils.import("resource://gre/modules/Services.jsm"); return Services.vc.compare(arguments[0], arguments[1]); """, script_args=[update['build_post']['version'], update['build_pre']['version']]) self.assertGreaterEqual(check, 0, 'The version of the upgraded build is higher or equal') # If a target version has been specified, check if it matches the updated build if self.target_version: self.assertEqual(update['build_post']['version'], self.target_version) # The post buildid should be identical with the buildid contained in the patch self.assertEqual(update['build_post']['buildid'], update['patch']['buildid']) # If a target buildid has been specified, check if it matches the updated build if self.target_buildid: self.assertEqual(update['build_post']['buildid'], self.target_buildid) # An upgrade should not change the builds locale self.assertEqual(update['build_post']['locale'], update['build_pre']['locale']) # Check that no application-wide add-ons have been disabled self.assertEqual(update['build_post']['disabled_addons'], update['build_pre']['disabled_addons']) # Bug 604364 - We do not support watershed releases yet. if update_available: self.download_update(about_window, wait_for_finish=False) self.assertNotEqual(self.software_update.active_update.type, update['patch']['type'], 'No further update of the same type gets offered: ' '{0} != {1}'.format( self.software_update.active_update.type, update['patch']['type'] )) update['success'] = True finally: about_window.close() def download_update(self, window, wait_for_finish=True, timeout=TIMEOUT_UPDATE_DOWNLOAD): """ Download the update patch. :param window: Instance of :class:`AboutWindow` or :class:`UpdateWizardDialog`. :param wait_for_finish: If True the function has to wait for the download to be finished. Optional, default to `True`. :param timeout: How long to wait for the download to finish. Optional, default to 360s. """ def download_via_update_wizard(dialog): """ Download the update via the old update wizard dialog. :param dialog: Instance of :class:`UpdateWizardDialog`. """ prefs = Preferences(lambda: self.marionette) prefs.set_pref(self.PREF_APP_UPDATE_ALTWINDOWTYPE, dialog.window_type) try: # If updates have already been found, proceed to download if dialog.wizard.selected_panel in [dialog.wizard.updates_found_basic, dialog.wizard.error_patching, ]: dialog.select_next_page() # If incompatible add-on are installed, skip over the wizard page # TODO: Remove once we no longer support version Firefox 45.0ESR if self.utils.compare_version(self.appinfo.version, '49.0a1') == -1: if dialog.wizard.selected_panel == dialog.wizard.incompatible_list: dialog.select_next_page() # Updates were stored in the cache, so no download is necessary if dialog.wizard.selected_panel in [dialog.wizard.finished, dialog.wizard.finished_background, ]: pass # Download the update elif dialog.wizard.selected_panel == dialog.wizard.downloading: if wait_for_finish: start_time = datetime.now() self.wait_for_download_finished(dialog, timeout) self.download_duration = (datetime.now() - start_time).total_seconds() Wait(self.marionette).until(lambda _: ( dialog.wizard.selected_panel in [dialog.wizard.finished, dialog.wizard.finished_background, ]), message='Final wizard page has been selected.') else: raise Exception('Invalid wizard page for downloading an update: {}'.format( dialog.wizard.selected_panel)) finally: prefs.restore_pref(self.PREF_APP_UPDATE_ALTWINDOWTYPE) # The old update wizard dialog has to be handled differently. It's necessary # for fallback updates and invalid add-on versions. if isinstance(window, UpdateWizardDialog): download_via_update_wizard(window) return if window.deck.selected_panel == window.deck.download_and_install: window.deck.download_and_install.button.click() # Wait for the download to start Wait(self.marionette).until(lambda _: ( window.deck.selected_panel != window.deck.download_and_install), message='Download of the update has been started.') if wait_for_finish: start_time = datetime.now() self.wait_for_download_finished(window, timeout) self.download_duration = (datetime.now() - start_time).total_seconds() def download_and_apply_available_update(self, force_fallback=False): """Checks, downloads, and applies an available update. :param force_fallback: Optional, if `True` invalidate current update status. Defaults to `False`. """ # Open the about window and check for updates about_window = self.browser.open_about_window() try: update_available = self.check_for_updates(about_window) self.assertTrue(update_available, "Available update has been found") # Download update and wait until it has been applied self.download_update(about_window) self.wait_for_update_applied(about_window) finally: self.updates[self.current_update_index]['patch'] = self.patch_info if force_fallback: # Set the downloaded update into failed state self.software_update.force_fallback() # Restart Firefox to apply the downloaded update self.restart() def download_and_apply_forced_update(self): # The update wizard dialog opens automatically after the restart but with a short delay dialog = Wait(self.marionette, ignored_exceptions=[NoSuchWindowException]).until( lambda _: self.windows.switch_to(lambda win: type(win) is UpdateWizardDialog) ) # In case of a broken complete update the about window has to be used if self.updates[self.current_update_index]['patch']['is_complete']: about_window = None try: self.assertEqual(dialog.wizard.selected_panel, dialog.wizard.error) dialog.close() # Open the about window and check for updates about_window = self.browser.open_about_window() update_available = self.check_for_updates(about_window) self.assertTrue(update_available, 'Available update has been found') # Download update and wait until it has been applied self.download_update(about_window) self.wait_for_update_applied(about_window) finally: if about_window: self.updates[self.current_update_index]['patch'] = self.patch_info else: try: self.assertEqual(dialog.wizard.selected_panel, dialog.wizard.error_patching) # Start downloading the fallback update self.download_update(dialog) dialog.close() finally: self.updates[self.current_update_index]['patch'] = self.patch_info # Restart Firefox to apply the update self.restart() def read_update_log(self): """Read the content of the update log file for the last update attempt.""" path = os.path.join(os.path.dirname(self.software_update.staging_directory), 'last-update.log') try: with open(path, 'rb') as f: return f.read().splitlines() except IOError as exc: self.logger.warning(str(exc)) return None def remove_downloaded_update(self): """Remove an already downloaded update from the update staging directory. Hereby not only remove the update subdir but everything below 'updates'. """ path = os.path.dirname(self.software_update.staging_directory) self.logger.info('Clean-up update staging directory: {}'.format(path)) mozfile.remove(path) def restore_config_files(self): # Reset channel-prefs.js file if modified try: if self.default_update_channel: path = self.default_update_channel['path'] self.logger.info('Restoring channel defaults for: {}'.format(path)) with open(path, 'w') as f: f.write(self.default_update_channel['content']) except IOError: self.logger.error('Failed to reset the default update channel.', exc_info=True) # Reset update-settings.ini file if modified try: if self.default_mar_channels: path = self.default_mar_channels['path'] self.logger.info('Restoring mar channel defaults for: {}'.format(path)) with open(path, 'w') as f: f.write(self.default_mar_channels['content']) except IOError: self.logger.error('Failed to reset the default mar channels.', exc_info=True) def wait_for_download_finished(self, window, timeout=TIMEOUT_UPDATE_DOWNLOAD): """ Waits until download is completed. :param window: Instance of :class:`AboutWindow` or :class:`UpdateWizardDialog`. :param timeout: How long to wait for the download to finish. Optional, default to 360 seconds. """ # The old update wizard dialog has to be handled differently. It's necessary # for fallback updates and invalid add-on versions. if isinstance(window, UpdateWizardDialog): Wait(self.marionette, timeout=timeout).until( lambda _: window.wizard.selected_panel != window.wizard.downloading, message='Download has been completed.') self.assertNotIn(window.wizard.selected_panel, [window.wizard.error, window.wizard.error_extra]) return Wait(self.marionette, timeout=timeout).until( lambda _: window.deck.selected_panel not in (window.deck.download_and_install, window.deck.downloading), message='Download has been completed.') self.assertNotEqual(window.deck.selected_panel, window.deck.download_failed) def wait_for_update_applied(self, about_window, timeout=TIMEOUT_UPDATE_APPLY): """ Waits until the downloaded update has been applied. :param about_window: Instance of :class:`AboutWindow`. :param timeout: How long to wait for the update to apply. Optional, default to 300 seconds """ Wait(self.marionette, timeout=timeout).until( lambda _: about_window.deck.selected_panel == about_window.deck.apply, message='Final wizard page has been selected.') # Wait for update to be staged because for update tests we modify the update # status file to enforce the fallback update. If we modify the file before # Firefox does, Firefox will override our change and we will have no fallback update. Wait(self.marionette, timeout=timeout).until( lambda _: 'applied' in self.software_update.active_update.state, message='Update has been applied.')
class UpdateTestCase(FirefoxTestCase): TIMEOUT_UPDATE_APPLY = 300 TIMEOUT_UPDATE_CHECK = 30 TIMEOUT_UPDATE_DOWNLOAD = 360 # For the old update wizard, the errors are displayed inside the dialog. For the # handling of updates in the about window the errors are displayed in new dialogs. # When the old wizard is open we have to set the preference, so the errors will be # shown as expected, otherwise we would have unhandled modal dialogs when errors are # raised. See: # http://mxr.mozilla.org/mozilla-central/source/toolkit/mozapps/update/nsUpdateService.js?rev=a9240b1eb2fb#4813 # http://mxr.mozilla.org/mozilla-central/source/toolkit/mozapps/update/nsUpdateService.js?rev=a9240b1eb2fb#4756 PREF_APP_UPDATE_ALTWINDOWTYPE = 'app.update.altwindowtype' def __init__(self, *args, **kwargs): FirefoxTestCase.__init__(self, *args, **kwargs) self.target_buildid = kwargs.pop('update_target_buildid') self.target_version = kwargs.pop('update_target_version') self.update_channel = kwargs.pop('update_channel') self.default_update_channel = None self.update_mar_channels = set(kwargs.pop('update_mar_channels')) self.default_mar_channels = None self.updates = [] def setUp(self, is_fallback=False): FirefoxTestCase.setUp(self) self.software_update = SoftwareUpdate(lambda: self.marionette) self.download_duration = None # Bug 604364 - Preparation to test multiple update steps self.current_update_index = 0 self.staging_directory = self.software_update.staging_directory # If requested modify the default update channel. It will be active # after the next restart of the application # Bug 1142805 - Modify file via Python directly if self.update_channel: # Backup the original content and the path of the channel-prefs.js file self.default_update_channel = { 'content': self.software_update.update_channel.file_contents, 'path': self.software_update.update_channel.file_path, } self.software_update.update_channel.default_channel = self.update_channel # If requested modify the list of allowed MAR channels # Bug 1142805 - Modify file via Python directly if self.update_mar_channels: # Backup the original content and the path of the update-settings.ini file self.default_mar_channels = { 'content': self.software_update.mar_channels.config_file_contents, 'path': self.software_update.mar_channels.config_file_path, } self.software_update.mar_channels.add_channels( self.update_mar_channels) # Bug 1142805 - Until we don't modify the channel-prefs.js and update-settings.ini # files before Firefox gets started, a restart of Firefox is necessary to # accept the new update channel. self.restart() # Dictionary which holds the information for each update self.updates = [{ 'build_pre': self.software_update.build_info, 'build_post': None, 'fallback': is_fallback, 'patch': {}, 'success': False, }] self.assertEqual(self.software_update.update_channel.default_channel, self.software_update.update_channel.channel) self.assertTrue( self.update_mar_channels.issubset( self.software_update.mar_channels.channels), 'Allowed MAR channels have been set: expected "{}" in "{}"'.format( ', '.join(self.update_mar_channels), ', '.join(self.software_update.mar_channels.channels))) # Check if the user has permissions to run the update self.assertTrue( self.software_update.allowed, 'Current user has permissions to update the application.') def tearDown(self): try: self.browser.tabbar.close_all_tabs( [self.browser.tabbar.selected_tab]) # Print results for now until we have treeherder integration output = pprint.pformat(self.updates) self.logger.info('Update test results: \n{}'.format(output)) finally: FirefoxTestCase.tearDown(self) self.restore_config_files() @property def patch_info(self): """ Returns information about the active update in the queue. :returns: A dictionary with information about the active patch """ patch = self.software_update.patch_info patch['download_duration'] = self.download_duration return patch def check_for_updates(self, about_window, timeout=TIMEOUT_UPDATE_CHECK): """Clicks on "Check for Updates" button, and waits for check to complete. :param about_window: Instance of :class:`AboutWindow`. :param timeout: How long to wait for the update check to finish. Optional, defaults to 60s. :returns: True, if an update is available. """ self.assertEqual(about_window.deck.selected_panel, about_window.deck.check_for_updates) about_window.deck.check_for_updates.button.click() Wait(self.marionette, timeout=self.TIMEOUT_UPDATE_CHECK).until( lambda _: about_window.deck.selected_panel not in (about_window.deck.check_for_updates, about_window.deck. checking_for_updates), message='Check for updates has been finished.') return about_window.deck.selected_panel != about_window.deck.no_updates_found def check_update_applied(self): self.updates[self.current_update_index][ 'build_post'] = self.software_update.build_info about_window = self.browser.open_about_window() try: update_available = self.check_for_updates(about_window) # No further updates should be offered now with the same update type if update_available: about_window.download(wait_for_finish=False) self.assertNotEqual( self.software_update.active_update.type, self.updates[self.current_update_index].type) # Check that the update has been applied correctly update = self.updates[self.current_update_index] # The upgraded version should be identical with the version given by # the update and we shouldn't have run a downgrade check = self.marionette.execute_script( """ Components.utils.import("resource://gre/modules/Services.jsm"); return Services.vc.compare(arguments[0], arguments[1]); """, script_args=[ update['build_post']['version'], update['build_pre']['version'] ]) self.assertGreaterEqual( check, 0, 'The version of the upgraded build is higher or equal') # If a target version has been specified, check if it matches the updated build if self.target_version: self.assertEqual(update['build_post']['version'], self.target_version) # The post buildid should be identical with the buildid contained in the patch self.assertEqual(update['build_post']['buildid'], update['patch']['buildid']) # If a target buildid has been specified, check if it matches the updated build if self.target_buildid: self.assertEqual(update['build_post']['buildid'], self.target_buildid) # An upgrade should not change the builds locale self.assertEqual(update['build_post']['locale'], update['build_pre']['locale']) # Check that no application-wide add-ons have been disabled self.assertEqual(update['build_post']['disabled_addons'], update['build_pre']['disabled_addons']) update['success'] = True finally: about_window.close() def download_update(self, window, wait_for_finish=True, timeout=TIMEOUT_UPDATE_DOWNLOAD): """ Download the update patch. :param window: Instance of :class:`AboutWindow` or :class:`UpdateWizardDialog`. :param wait_for_finish: If True the function has to wait for the download to be finished. Optional, default to `True`. :param timeout: How long to wait for the download to finish. Optional, default to 360s. """ def download_via_update_wizard(dialog): """ Download the update via the old update wizard dialog. :param dialog: Instance of :class:`UpdateWizardDialog`. """ prefs = Preferences(lambda: self.marionette) prefs.set_pref(self.PREF_APP_UPDATE_ALTWINDOWTYPE, dialog.window_type) try: # If updates have already been found, proceed to download if dialog.wizard.selected_panel in [ dialog.wizard.updates_found_basic, dialog.wizard.updates_found_billboard, dialog.wizard.error_patching, ]: dialog.select_next_page() # If incompatible add-on are installed, skip over the wizard page if dialog.wizard.selected_panel == dialog.wizard.incompatible_list: dialog.select_next_page() # Updates were stored in the cache, so no download is necessary if dialog.wizard.selected_panel in [ dialog.wizard.finished, dialog.wizard.finished_background, ]: pass # Download the update elif dialog.wizard.selected_panel == dialog.wizard.downloading: if wait_for_finish: start_time = datetime.now() self.wait_for_download_finished(dialog, timeout) self.download_duration = (datetime.now() - start_time).total_seconds() Wait(self.marionette).until( lambda _: (dialog.wizard.selected_panel in [ dialog.wizard.finished, dialog.wizard.finished_background, ]), message='Final wizard page has been selected.') else: raise Exception( 'Invalid wizard page for downloading an update: {}'. format(dialog.wizard.selected_panel)) finally: prefs.restore_pref(self.PREF_APP_UPDATE_ALTWINDOWTYPE) # The old update wizard dialog has to be handled differently. It's necessary # for fallback updates and invalid add-on versions. if isinstance(window, UpdateWizardDialog): download_via_update_wizard(window) return if window.deck.selected_panel == window.deck.download_and_install: window.deck.download_and_install.button.click() # Wait for the download to start Wait(self.marionette).until( lambda _: (window.deck.selected_panel != window.deck. download_and_install)) # If there are incompatible addons, handle the update via the old software update dialog if window.deck.selected_panel == window.deck.apply_billboard: # Clicking the update button will open the old update wizard dialog dialog = self.browser.open_window( callback=lambda _: window.deck.update_button.click(), expected_window_class=UpdateWizardDialog) Wait(self.marionette).until(lambda _: dialog.wizard.selected_panel == dialog.wizard.updates_found_basic) download_via_update_wizard(dialog) dialog.close() return if wait_for_finish: start_time = datetime.now() self.wait_for_download_finished(window, timeout) self.download_duration = (datetime.now() - start_time).total_seconds() def download_and_apply_available_update(self, force_fallback=False): """Checks, downloads, and applies an available update. :param force_fallback: Optional, if `True` invalidate current update status. Defaults to `False`. """ # Open the about window and check for updates about_window = self.browser.open_about_window() try: update_available = self.check_for_updates(about_window) self.assertTrue(update_available, "Available update has been found") # Download update and wait until it has been applied self.download_update(about_window) self.wait_for_update_applied(about_window) finally: self.updates[self.current_update_index]['patch'] = self.patch_info if force_fallback: # Set the downloaded update into failed state self.software_update.force_fallback() # Restart Firefox to apply the downloaded update self.restart() def download_and_apply_forced_update(self): # The update wizard dialog opens automatically after the restart dialog = self.windows.switch_to( lambda win: type(win) is UpdateWizardDialog) # In case of a broken complete update the about window has to be used if self.updates[self.current_update_index]['patch']['is_complete']: about_window = None try: self.assertEqual(dialog.wizard.selected_panel, dialog.wizard.error) dialog.close() # Open the about window and check for updates about_window = self.browser.open_about_window() update_available = self.check_for_updates(about_window) self.assertTrue(update_available, 'Available update has been found') # Download update and wait until it has been applied self.download(about_window) self.wait_for_update_applied(about_window) finally: if about_window: self.updates[ self.current_update_index]['patch'] = self.patch_info else: try: self.assertEqual(dialog.wizard.selected_panel, dialog.wizard.error_patching) # Start downloading the fallback update self.download_update(dialog) dialog.close() finally: self.updates[ self.current_update_index]['patch'] = self.patch_info # Restart Firefox to apply the update self.restart() def restore_config_files(self): # Reset channel-prefs.js file if modified try: if self.default_update_channel: path = self.default_update_channel['path'] self.logger.info( 'Restoring channel defaults for: {}'.format(path)) with open(path, 'w') as f: f.write(self.default_update_channel['content']) except IOError: self.logger.error('Failed to reset the default update channel.', exc_info=True) # Reset update-settings.ini file if modified try: if self.default_mar_channels: path = self.default_mar_channels['path'] self.logger.info( 'Restoring mar channel defaults for: {}'.format(path)) with open(path, 'w') as f: f.write(self.default_mar_channels['content']) except IOError: self.logger.error('Failed to reset the default mar channels.', exc_info=True) def wait_for_download_finished(self, window, timeout=TIMEOUT_UPDATE_DOWNLOAD): """ Waits until download is completed. :param window: Instance of :class:`AboutWindow` or :class:`UpdateWizardDialog`. :param timeout: How long to wait for the download to finish. Optional, default to 360 seconds. """ # The old update wizard dialog has to be handled differently. It's necessary # for fallback updates and invalid add-on versions. if isinstance(window, UpdateWizardDialog): Wait(self.marionette, timeout=timeout).until(lambda _: window.wizard.selected_panel != window.wizard.downloading, message='Download has been completed.') self.assertNotIn(window.wizard.selected_panel, [window.wizard.error, window.wizard.error_extra]) return Wait(self.marionette, timeout=timeout).until( lambda _: window.deck.selected_panel not in (window.deck.download_and_install, window.deck.downloading), message='Download has been completed.') self.assertNotEqual(window.deck.selected_panel, window.deck.download_failed) def wait_for_update_applied(self, about_window, timeout=TIMEOUT_UPDATE_APPLY): """ Waits until the downloaded update has been applied. :param about_window: Instance of :class:`AboutWindow`. :param timeout: How long to wait for the update to apply. Optional, default to 300 seconds """ Wait(self.marionette, timeout=timeout).until( lambda _: about_window.deck.selected_panel == about_window.deck. apply, message='Final wizard page has been selected.') # Wait for update to be staged because for update tests we modify the update # status file to enforce the fallback update. If we modify the file before # Firefox does, Firefox will override our change and we will have no fallback update. Wait(self.marionette, timeout=timeout).until( lambda _: 'applied' in self.software_update.active_update.state, message='Update has been applied.')
def setUp(self): FirefoxTestCase.setUp(self) self.software_update = SoftwareUpdate(lambda: self.marionette) self.saved_mar_channels = self.software_update.mar_channels.channels self.software_update.mar_channels.channels = set(['expected', 'channels'])
class UpdateTestCase(FirefoxTestCase): def __init__(self, *args, **kwargs): FirefoxTestCase.__init__(self, *args, **kwargs) self.target_buildid = kwargs.pop('update_target_buildid') self.target_version = kwargs.pop('update_target_version') self.update_channel = kwargs.pop('update_channel') self.default_update_channel = None self.update_mar_channels = set(kwargs.pop('update_mar_channels')) self.default_mar_channels = None self.updates = [] def setUp(self, is_fallback=False): FirefoxTestCase.setUp(self) self.software_update = SoftwareUpdate(lambda: self.marionette) # Bug 604364 - Preparation to test multiple update steps self.current_update_index = 0 self.staging_directory = self.software_update.staging_directory # If requested modify the default update channel. It will be active # after the next restart of the application # Bug 1142805 - Modify file via Python directly if self.update_channel: # Backup the original content and the path of the channel-prefs.js file self.default_update_channel = { 'content': self.software_update.update_channel.file_contents, 'path': self.software_update.update_channel.file_path, } self.software_update.update_channel.default_channel = self.update_channel # If requested modify the list of allowed MAR channels # Bug 1142805 - Modify file via Python directly if self.update_mar_channels: # Backup the original content and the path of the update-settings.ini file self.default_mar_channels = { 'content': self.software_update.mar_channels.config_file_contents, 'path': self.software_update.mar_channels.config_file_path, } self.software_update.mar_channels.add_channels( self.update_mar_channels) # Bug 1142805 - Until we don't modify the channel-prefs.js and update-settings.ini # files before Firefox gets started, a restart of Firefox is necessary to # accept the new update channel. self.restart() # Dictionary which holds the information for each update self.updates = [{ 'build_pre': self.software_update.build_info, 'build_post': None, 'fallback': is_fallback, 'patch': {}, 'success': False, }] self.assertEqual(self.software_update.update_channel.default_channel, self.software_update.update_channel.channel) self.assertTrue( self.update_mar_channels.issubset( self.software_update.mar_channels.channels), 'Allowed MAR channels have been set: expected "{}" in "{}"'.format( ', '.join(self.update_mar_channels), ', '.join(self.software_update.mar_channels.channels))) # Check if the user has permissions to run the update self.assertTrue( self.software_update.allowed, 'Current user has permissions to update the application.') def assert_update_applied(self, update_status): """Checks if an update has been applied correctly. :param update_status: All the data collected during the update process """ # Get the information from the last update info = update_status[self.current_update_index] # The upgraded version should be identical with the version given by # the update and we shouldn't have run a downgrade check = self.marionette.execute_script( """ Components.utils.import("resource://gre/modules/Services.jsm"); return Services.vc.compare(arguments[0], arguments[1]); """, script_args=[ info['build_post']['version'], info['build_pre']['version'] ]) self.assertGreaterEqual( check, 0, 'The version of the upgraded build is higher or equal') # If a target version has been specified, check if it matches the updated build if self.target_version: self.assertEqual(info['build_post']['version'], self.target_version) # The post buildid should be identical with the buildid contained in the patch self.assertEqual(info['build_post']['buildid'], info['patch']['buildid']) # If a target buildid has been specified, check if it matches the updated build if self.target_buildid: self.assertEqual(info['build_post']['buildid'], self.target_buildid) # An upgrade should not change the builds locale self.assertEqual(info['build_post']['locale'], info['build_pre']['locale']) # Check that no application-wide add-ons have been disabled self.assertEqual(info['build_post']['disabled_addons'], info['build_pre']['disabled_addons']) def check_update_applied(self): self.updates[self.current_update_index][ 'build_post'] = self.software_update.build_info about_window = self.browser.open_about_window() try: update_available = about_window.check_for_updates() # No further updates should be offered now with the same update type if update_available: about_window.download(wait_for_finish=False) self.assertNotEqual( self.software_update.active_update.type, self.updates[self.current_update_index].type) # Check that updates have been applied correctly self.assert_update_applied(self.updates) self.updates[self.current_update_index]['success'] = True finally: about_window.close() def download_and_apply_available_update(self, force_fallback=False): """Checks, downloads, and applies an available update. :param force_fallback: Optional, if `True` invalidate current update status. Defaults to `False`. """ # Open the about window and check for updates about_window = self.browser.open_about_window() try: update_available = about_window.check_for_updates() self.assertTrue(update_available, "Available update has been found") # Download update and wait until it has been applied about_window.download() about_window.wait_for_update_applied() finally: self.updates[ self.current_update_index]['patch'] = about_window.patch_info if force_fallback: # Set the downloaded update into failed state self.software_update.force_fallback() # Restart Firefox to apply the downloaded update self.restart() def download_and_apply_forced_update(self): # The update wizard dialog opens automatically after the restart dialog = self.windows.switch_to( lambda win: type(win) is UpdateWizardDialog) # In case of a broken complete update the about window has to be used if self.updates[self.current_update_index]['patch']['is_complete']: about_window = None try: self.assertEqual(dialog.wizard.selected_panel, dialog.wizard.error) dialog.close() # Open the about window and check for updates about_window = self.browser.open_about_window() update_available = about_window.check_for_updates() self.assertTrue(update_available, 'Available update has been found') # Download update and wait until it has been applied about_window.download() about_window.wait_for_update_applied() finally: if about_window: self.updates[self.current_update_index][ 'patch'] = about_window.patch_info else: try: self.assertEqual(dialog.wizard.selected_panel, dialog.wizard.error_patching) # Start downloading the fallback update dialog.download() dialog.close() finally: self.updates[ self.current_update_index]['patch'] = dialog.patch_info # Restart Firefox to apply the update self.restart() def restore_config_files(self): # Reset channel-prefs.js file if modified try: if self.default_update_channel: path = self.default_update_channel['path'] self.logger.info( 'Restoring channel defaults for: {}'.format(path)) with open(path, 'w') as f: f.write(self.default_update_channel['content']) except IOError: self.logger.error('Failed to reset the default update channel.', exc_info=True) # Reset update-settings.ini file if modified try: if self.default_mar_channels: path = self.default_mar_channels['path'] self.logger.info( 'Restoring mar channel defaults for: {}'.format(path)) with open(path, 'w') as f: f.write(self.default_mar_channels['content']) except IOError: self.logger.error('Failed to reset the default mar channels.', exc_info=True) def tearDown(self): try: self.browser.tabbar.close_all_tabs( [self.browser.tabbar.selected_tab]) # Print results for now until we have treeherder integration output = pprint.pformat(self.updates) self.logger.info('Update test results: \n{}'.format(output)) finally: FirefoxTestCase.tearDown(self) self.restore_config_files()
class UpdateTestCase(PuppeteerMixin, MarionetteTestCase): TIMEOUT_UPDATE_APPLY = 300 TIMEOUT_UPDATE_CHECK = 30 TIMEOUT_UPDATE_DOWNLOAD = 720 # For the old update wizard, the errors are displayed inside the dialog. For the # handling of updates in the about window the errors are displayed in new dialogs. # When the old wizard is open we have to set the preference, so the errors will be # shown as expected, otherwise we would have unhandled modal dialogs when errors are # raised. See: # http://mxr.mozilla.org/mozilla-central/source/toolkit/mozapps/update/nsUpdateService.js?rev=a9240b1eb2fb#4813 # http://mxr.mozilla.org/mozilla-central/source/toolkit/mozapps/update/nsUpdateService.js?rev=a9240b1eb2fb#4756 PREF_APP_UPDATE_ALTWINDOWTYPE = 'app.update.altwindowtype' def __init__(self, *args, **kwargs): super(UpdateTestCase, self).__init__(*args, **kwargs) self.update_channel = kwargs.pop('update_channel') self.update_mar_channels = set(kwargs.pop('update_mar_channels')) self.target_buildid = kwargs.pop('update_target_buildid') self.target_version = kwargs.pop('update_target_version') # Bug 604364 - Preparation to test multiple update steps self.current_update_index = 0 self.download_duration = None self.updates = [] def setUp(self, is_fallback=False): super(UpdateTestCase, self).setUp() self.software_update = SoftwareUpdate(self.marionette) # If a custom update channel has to be set, force a restart of # Firefox to actually get it applied as a default pref. Use the clean # option to force a non in_app restart, which would allow Firefox to # dump the logs to the console. if self.update_channel: self.software_update.update_channel = self.update_channel self.restart(clean=True) self.assertEqual(self.software_update.update_channel, self.update_channel) # If requested modify the list of allowed MAR channels if self.update_mar_channels: self.software_update.mar_channels.add_channels(self.update_mar_channels) self.assertTrue(self.update_mar_channels.issubset( self.software_update.mar_channels.channels), 'Allowed MAR channels have been set: expected "{}" in "{}"'.format( ', '.join(self.update_mar_channels), ', '.join(self.software_update.mar_channels.channels))) # Ensure that there exists no already partially downloaded update self.remove_downloaded_update() # Dictionary which holds the information for each update self.updates = [{ 'build_pre': self.software_update.build_info, 'build_post': None, 'fallback': is_fallback, 'patch': {}, 'success': False, }] # Check if the user has permissions to run the update self.assertTrue(self.software_update.allowed, 'Current user has permissions to update the application.') def tearDown(self): try: self.browser.tabbar.close_all_tabs([self.browser.tabbar.selected_tab]) # Add content of the update log file for detailed failures when applying an update self.updates[self.current_update_index]['update_log'] = self.read_update_log() # Print results for now until we have treeherder integration output = pprint.pformat(self.updates) self.logger.info('Update test results: \n{}'.format(output)) finally: super(UpdateTestCase, self).tearDown() # Ensure that no trace of an partially downloaded update remain self.remove_downloaded_update() @property def patch_info(self): """ Returns information about the active update in the queue. :returns: A dictionary with information about the active patch """ patch = self.software_update.patch_info patch['download_duration'] = self.download_duration return patch def check_for_updates(self, about_window, timeout=TIMEOUT_UPDATE_CHECK): """Clicks on "Check for Updates" button, and waits for check to complete. :param about_window: Instance of :class:`AboutWindow`. :param timeout: How long to wait for the update check to finish. Optional, defaults to 60s. :returns: True, if an update is available. """ self.assertEqual(about_window.deck.selected_panel, about_window.deck.check_for_updates) about_window.deck.check_for_updates.button.click() Wait(self.marionette, timeout=self.TIMEOUT_UPDATE_CHECK).until( lambda _: about_window.deck.selected_panel not in (about_window.deck.check_for_updates, about_window.deck.checking_for_updates), message='Check for updates has been finished.') return about_window.deck.selected_panel != about_window.deck.no_updates_found def check_update_applied(self): """Check that the update has been applied correctly""" update = self.updates[self.current_update_index] update['build_post'] = self.software_update.build_info about_window = self.browser.open_about_window() try: # Bug 604364 - We do not support watershed releases yet. update_available = self.check_for_updates(about_window) self.assertFalse(update_available, 'Additional update found due to watershed release {}'.format( update['build_post']['version'])) # The upgraded version should be identical with the version given by # the update and we shouldn't have run a downgrade check = self.marionette.execute_script(""" Components.utils.import("resource://gre/modules/Services.jsm"); return Services.vc.compare(arguments[0], arguments[1]); """, script_args=[update['build_post']['version'], update['build_pre']['version']]) self.assertGreaterEqual(check, 0, 'The version of the upgraded build is higher or equal') # If a target version has been specified, check if it matches the updated build if self.target_version: self.assertEqual(update['build_post']['version'], self.target_version) # The post buildid should be identical with the buildid contained in the patch self.assertEqual(update['build_post']['buildid'], update['patch']['buildid']) # If a target buildid has been specified, check if it matches the updated build if self.target_buildid: self.assertEqual(update['build_post']['buildid'], self.target_buildid) # An upgrade should not change the builds locale self.assertEqual(update['build_post']['locale'], update['build_pre']['locale']) # Check that no application-wide add-ons have been disabled self.assertEqual(update['build_post']['disabled_addons'], update['build_pre']['disabled_addons']) update['success'] = True finally: about_window.close() def download_update(self, window, wait_for_finish=True, timeout=TIMEOUT_UPDATE_DOWNLOAD): """ Download the update patch. :param window: Instance of :class:`AboutWindow` or :class:`UpdateWizardDialog`. :param wait_for_finish: If True the function has to wait for the download to be finished. Optional, default to `True`. :param timeout: How long to wait for the download to finish. Optional, default to 360s. """ def download_via_update_wizard(dialog): """ Download the update via the old update wizard dialog. :param dialog: Instance of :class:`UpdateWizardDialog`. """ self.marionette.set_pref(self.PREF_APP_UPDATE_ALTWINDOWTYPE, dialog.window_type) try: # If updates have already been found, proceed to download if dialog.wizard.selected_panel in [dialog.wizard.updates_found_basic, dialog.wizard.error_patching, ]: dialog.select_next_page() # If incompatible add-on are installed, skip over the wizard page # TODO: Remove once we no longer support version Firefox 45.0ESR if self.puppeteer.utils.compare_version(self.puppeteer.appinfo.version, '49.0a1') == -1: if dialog.wizard.selected_panel == dialog.wizard.incompatible_list: dialog.select_next_page() # Updates were stored in the cache, so no download is necessary if dialog.wizard.selected_panel in [dialog.wizard.finished, dialog.wizard.finished_background, ]: pass # Download the update elif dialog.wizard.selected_panel == dialog.wizard.downloading: if wait_for_finish: start_time = datetime.now() self.wait_for_download_finished(dialog, timeout) self.download_duration = (datetime.now() - start_time).total_seconds() Wait(self.marionette).until(lambda _: ( dialog.wizard.selected_panel in [dialog.wizard.finished, dialog.wizard.finished_background, ]), message='Final wizard page has been selected.') else: raise Exception('Invalid wizard page for downloading an update: {}'.format( dialog.wizard.selected_panel)) finally: self.marionette.clear_pref(self.PREF_APP_UPDATE_ALTWINDOWTYPE) # The old update wizard dialog has to be handled differently. It's necessary # for fallback updates and invalid add-on versions. if isinstance(window, UpdateWizardDialog): download_via_update_wizard(window) return if window.deck.selected_panel == window.deck.download_and_install: window.deck.download_and_install.button.click() # Wait for the download to start Wait(self.marionette).until(lambda _: ( window.deck.selected_panel != window.deck.download_and_install), message='Download of the update has been started.') if wait_for_finish: start_time = datetime.now() self.wait_for_download_finished(window, timeout) self.download_duration = (datetime.now() - start_time).total_seconds() def download_and_apply_available_update(self, force_fallback=False): """Checks, downloads, and applies an available update. :param force_fallback: Optional, if `True` invalidate current update status. Defaults to `False`. """ # Open the about window and check for updates about_window = self.browser.open_about_window() try: update_available = self.check_for_updates(about_window) self.assertTrue(update_available, "Available update has been found") # Download update and wait until it has been applied self.download_update(about_window) self.wait_for_update_applied(about_window) finally: self.updates[self.current_update_index]['patch'] = self.patch_info if force_fallback: # Set the downloaded update into failed state self.software_update.force_fallback() # Restart Firefox to apply the downloaded update self.restart() def download_and_apply_forced_update(self): # The update wizard dialog opens automatically after the restart but with a short delay dialog = Wait(self.marionette, ignored_exceptions=[NoSuchWindowException]).until( lambda _: self.puppeteer.windows.switch_to(lambda win: type(win) is UpdateWizardDialog) ) # In case of a broken complete update the about window has to be used if self.updates[self.current_update_index]['patch']['is_complete']: about_window = None try: self.assertEqual(dialog.wizard.selected_panel, dialog.wizard.error) dialog.close() # Open the about window and check for updates about_window = self.browser.open_about_window() update_available = self.check_for_updates(about_window) self.assertTrue(update_available, 'Available update has been found') # Download update and wait until it has been applied self.download_update(about_window) self.wait_for_update_applied(about_window) finally: if about_window: self.updates[self.current_update_index]['patch'] = self.patch_info else: try: self.assertEqual(dialog.wizard.selected_panel, dialog.wizard.error_patching) # Start downloading the fallback update self.download_update(dialog) dialog.close() finally: self.updates[self.current_update_index]['patch'] = self.patch_info # Restart Firefox to apply the update self.restart() def read_update_log(self): """Read the content of the update log file for the last update attempt.""" path = os.path.join(os.path.dirname(self.software_update.staging_directory), 'last-update.log') try: with open(path, 'rb') as f: return f.read().splitlines() except IOError as exc: self.logger.warning(str(exc)) return None def remove_downloaded_update(self): """Remove an already downloaded update from the update staging directory. Hereby not only remove the update subdir but everything below 'updates'. """ path = os.path.dirname(self.software_update.staging_directory) self.logger.info('Clean-up update staging directory: {}'.format(path)) mozfile.remove(path) def wait_for_download_finished(self, window, timeout=TIMEOUT_UPDATE_DOWNLOAD): """ Waits until download is completed. :param window: Instance of :class:`AboutWindow` or :class:`UpdateWizardDialog`. :param timeout: How long to wait for the download to finish. Optional, default to 360 seconds. """ # The old update wizard dialog has to be handled differently. It's necessary # for fallback updates and invalid add-on versions. if isinstance(window, UpdateWizardDialog): Wait(self.marionette, timeout=timeout).until( lambda _: window.wizard.selected_panel != window.wizard.downloading, message='Download has been completed.') self.assertNotIn(window.wizard.selected_panel, [window.wizard.error, window.wizard.error_extra]) return Wait(self.marionette, timeout=timeout).until( lambda _: window.deck.selected_panel not in (window.deck.download_and_install, window.deck.downloading), message='Download has been completed.') self.assertNotEqual(window.deck.selected_panel, window.deck.download_failed) def wait_for_update_applied(self, about_window, timeout=TIMEOUT_UPDATE_APPLY): """ Waits until the downloaded update has been applied. :param about_window: Instance of :class:`AboutWindow`. :param timeout: How long to wait for the update to apply. Optional, default to 300 seconds """ Wait(self.marionette, timeout=timeout).until( lambda _: about_window.deck.selected_panel == about_window.deck.apply, message='Final wizard page has been selected.') # Wait for update to be staged because for update tests we modify the update # status file to enforce the fallback update. If we modify the file before # Firefox does, Firefox will override our change and we will have no fallback update. Wait(self.marionette, timeout=timeout).until( lambda _: 'applied' in self.software_update.active_update.state, message='Update has been applied.')
class TestSoftwareUpdate(PuppeteerMixin, MarionetteTestCase): def setUp(self): super(TestSoftwareUpdate, self).setUp() self.software_update = SoftwareUpdate(self.marionette) self.saved_mar_channels = self.software_update.mar_channels.channels self.software_update.mar_channels.channels = set(['expected', 'channels']) def tearDown(self): try: self.software_update.mar_channels.channels = self.saved_mar_channels finally: super(TestSoftwareUpdate, self).tearDown() def test_abi(self): self.assertTrue(self.software_update.ABI) def test_allowed(self): self.assertTrue(self.software_update.allowed) def test_build_info(self): self.software_update.update_url = self.marionette.absolute_url( 'update/snippet_empty.xml?product=%PRODUCT%&version=%VERSION%&' 'buildid=%BUILD_ID%&locale=%LOCALE%&channel=%CHANNEL%') try: build_info = self.software_update.build_info self.assertEqual(build_info['disabled_addons'], None) self.assertIn('Mozilla/', build_info['user_agent']) self.assertEqual(build_info['mar_channels'], set(['expected', 'channels'])) self.assertTrue(build_info['version']) self.assertTrue(build_info['buildid'].isdigit()) self.assertTrue(build_info['locale']) self.assertIn('force=1', build_info['update_url']) self.assertIn('xml', build_info['update_snippet']) self.assertEqual(build_info['channel'], self.software_update.update_channel) finally: # Restart Firefox to reset the custom update url self.restart(clean=True) def test_force_fallback(self): status_file = os.path.join(self.software_update.staging_directory, 'update.status') try: self.software_update.force_fallback() with open(status_file, 'r') as f: content = f.read() self.assertEqual(content, 'failed: 6\n') finally: os.remove(status_file) def test_get_update_url(self): update_url = self.software_update.get_formatted_update_url() self.assertIn('Firefox', update_url) self.assertNotIn('force=1', update_url) update_url = self.software_update.get_formatted_update_url(True) self.assertIn('Firefox', update_url) self.assertIn('force=1', update_url) def test_os_version(self): self.assertTrue(self.software_update.os_version) def test_staging_directory(self): self.assertTrue(self.software_update.staging_directory)