PKxHLfirefox_ui_harness/__init__.py# This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. __version__ = '1.3.0' import cli_functional import cli_update PK›xH22 firefox_ui_harness/cli_update.py#!/usr/bin/env python # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. from marionette.runtests import cli as mn_cli from firefox_ui_harness.arguments import UpdateArguments from firefox_ui_harness.runners import UpdateTestRunner def cli(args=None): mn_cli(runner_class=UpdateTestRunner, parser_class=UpdateArguments, args=args, ) if __name__ == '__main__': cli() PK›xHV>>$firefox_ui_harness/cli_functional.py#!/usr/bin/env python # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. from marionette.runtests import cli as mn_cli from firefox_ui_harness.arguments import FirefoxUIArguments from firefox_ui_harness.runners import FirefoxUITestRunner def cli(args=None): mn_cli(runner_class=FirefoxUITestRunner, parser_class=FirefoxUIArguments, args=args, ) if __name__ == '__main__': cli() PKxHMJJfirefox_ui_harness/testcases.py# This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. import pprint from datetime import datetime from marionette import MarionetteTestCase from marionette_driver import Wait from firefox_puppeteer.api.prefs import Preferences from firefox_puppeteer.api.software_update import SoftwareUpdate from firefox_puppeteer.testcases import BaseFirefoxTestCase from firefox_puppeteer.ui.update_wizard import UpdateWizardDialog class FirefoxTestCase(BaseFirefoxTestCase, MarionetteTestCase): """ Integrate MarionetteTestCase with BaseFirefoxTestCase by reordering MRO """ pass 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 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: super(UpdateTestCase, self).tearDown() 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: self.download_update(about_window, 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_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 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.') PK4H^  &firefox_ui_harness/arguments/update.py# This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. from base import FirefoxUIArguments class UpdateBaseArguments(object): name = 'Firefox UI Update Tests' args = [ [['--update-allow-mar-channel'], { 'dest': 'update_mar_channels', 'default': [], 'action': 'append', 'metavar': 'MAR_CHANNEL', 'help': 'Additional MAR channel to be allowed for updates, ' 'e.g. "firefox-mozilla-beta" for updating a release ' 'build to the latest beta build.' }], [['--update-channel'], { 'dest': 'update_channel', 'metavar': 'CHANNEL', 'help': 'Channel to use for the update check.' }], [['--update-direct-only'], { 'dest': 'update_direct_only', 'default': False, 'action': 'store_true', 'help': 'Only perform a direct update' }], [['--update-fallback-only'], { 'dest': 'update_fallback_only', 'default': False, 'action': 'store_true', 'help': 'Only perform a fallback update' }], [['--update-override-url'], { 'dest': 'update_override_url', 'metavar': 'URL', 'help': 'Force specified URL to use for update checks.' }], [['--update-target-version'], { 'dest': 'update_target_version', 'metavar': 'VERSION', 'help': 'Version of the updated build.' }], [['--update-target-buildid'], { 'dest': 'update_target_buildid', 'metavar': 'BUILD_ID', 'help': 'Build ID of the updated build.' }], ] def verify_usage_handler(self, args): if args.update_direct_only and args.update_fallback_only: raise ValueError('Arguments --update-direct-only and --update-fallback-only ' 'are mutually exclusive.') class UpdateArguments(FirefoxUIArguments): def __init__(self, **kwargs): FirefoxUIArguments.__init__(self, **kwargs) self.register_argument_container(UpdateBaseArguments()) PK4HD~II(firefox_ui_harness/arguments/__init__.py# This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. from firefox_ui_harness.arguments.base import FirefoxUIArguments from firefox_ui_harness.arguments.update import UpdateArguments PK›xHe  $firefox_ui_harness/arguments/base.py# This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. from marionette import BaseMarionetteArguments class FirefoxUIBaseArguments(object): name = 'Firefox UI Tests' args = [] class FirefoxUIArguments(BaseMarionetteArguments): def __init__(self, **kwargs): BaseMarionetteArguments.__init__(self, **kwargs) self.register_argument_container(FirefoxUIBaseArguments()) PKxHGmp, , $firefox_ui_harness/runners/update.py# This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. import sys import mozfile import mozinstall from firefox_ui_harness.runners import FirefoxUITestRunner from firefox_ui_harness.testcases import UpdateTestCase DEFAULT_PREFS = { 'app.update.log': True, 'startup.homepage_override_url': 'about:blank', } class UpdateTestRunner(FirefoxUITestRunner): def __init__(self, **kwargs): FirefoxUITestRunner.__init__(self, **kwargs) self.original_bin = self.bin self.prefs.update(DEFAULT_PREFS) # In case of overriding the update URL, set the appropriate preference override_url = kwargs.pop('update_override_url', None) if override_url: self.prefs.update({'app.update.url.override': override_url}) self.run_direct_update = not kwargs.pop('update_fallback_only', False) self.run_fallback_update = not kwargs.pop('update_direct_only', False) self.test_handlers = [UpdateTestCase] def run_tests(self, tests): # Used to store the last occurred exception because we execute # run_tests() multiple times self.exc_info = None failed = 0 source_folder = self.get_application_folder(self.original_bin) results = {} def _run_tests(tags): application_folder = None try: # Backup current tags test_tags = self.test_tags application_folder = self.duplicate_application(source_folder) self.bin = mozinstall.get_binary(application_folder, 'Firefox') self.test_tags = tags FirefoxUITestRunner.run_tests(self, tests) except Exception: self.exc_info = sys.exc_info() self.logger.error('Failure during execution of the update test.', exc_info=self.exc_info) finally: self.test_tags = test_tags self.logger.info('Removing copy of the application at "%s"' % application_folder) try: mozfile.remove(application_folder) except IOError as e: self.logger.error('Cannot remove copy of application: "%s"' % str(e)) # Run direct update tests if wanted if self.run_direct_update: _run_tests(tags=['direct']) failed += self.failed results['Direct'] = False if self.failed else True # Run fallback update tests if wanted if self.run_fallback_update: _run_tests(tags=['fallback']) failed += self.failed results['Fallback'] = False if self.failed else True self.logger.info("Summary of update tests:") for test_type, result in results.iteritems(): self.logger.info("\t%s update test ran and %s" % (test_type, 'PASSED' if result else 'FAILED')) # Combine failed tests for all run_test() executions self.failed = failed # If exceptions happened, re-throw the last one if self.exc_info: ex_type, exception, tb = self.exc_info raise ex_type, exception, tb PKuuH^tGG&firefox_ui_harness/runners/__init__.py# This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. from firefox_ui_harness.runners.base import FirefoxUITestRunner from firefox_ui_harness.runners.update import UpdateTestRunner PKxHV "firefox_ui_harness/runners/base.py# This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. import os import shutil import tempfile import mozfile import mozinfo from marionette import BaseMarionetteTestRunner from firefox_ui_harness.testcases import FirefoxTestCase class FirefoxUITestRunner(BaseMarionetteTestRunner): def __init__(self, **kwargs): BaseMarionetteTestRunner.__init__(self, **kwargs) # select the appropriate GeckoInstance self.app = 'fxdesktop' self.test_handlers = [FirefoxTestCase] def duplicate_application(self, application_folder): """Creates a copy of the specified binary.""" if self.workspace: target_folder = os.path.join(self.workspace_path, 'application.copy') else: target_folder = tempfile.mkdtemp('.application.copy') self.logger.info('Creating a copy of the application at "%s".' % target_folder) mozfile.remove(target_folder) shutil.copytree(application_folder, target_folder) return target_folder def get_application_folder(self, binary): """Returns the directory of the application.""" if mozinfo.isMac: end_index = binary.find('.app') + 4 return binary[:end_index] else: return os.path.dirname(binary) PKxH-dd2firefox_ui_harness-1.3.0.dist-info/DESCRIPTION.rstCustom Marionette runner classes and entry scripts for Firefox Desktop specific Marionette tests. PKxHA/3firefox_ui_harness-1.3.0.dist-info/entry_points.txt [console_scripts] firefox-ui-functional = firefox_ui_harness.cli_functional:cli firefox-ui-update = firefox_ui_harness.cli_update:cli PKxHns0firefox_ui_harness-1.3.0.dist-info/metadata.json{"extensions": {"python.commands": {"wrap_console": {"firefox-ui-functional": "firefox_ui_harness.cli_functional:cli", "firefox-ui-update": "firefox_ui_harness.cli_update:cli"}}, "python.details": {"contacts": [{"email": "tools-marionette@lists.mozilla.org", "name": "Auto-tools", "role": "author"}], "document_names": {"description": "DESCRIPTION.rst"}, "project_urls": {"Home": "https://wiki.mozilla.org/Auto-tools/Projects/Marionette/Harnesses/FirefoxUI"}}, "python.exports": {"console_scripts": {"firefox-ui-functional": "firefox_ui_harness.cli_functional:cli", "firefox-ui-update": "firefox_ui_harness.cli_update:cli"}}}, "extras": [], "generator": "bdist_wheel (0.26.0)", "keywords": ["mozilla"], "license": "MPL", "metadata_version": "2.0", "name": "firefox-ui-harness", "run_requires": [{"requires": ["firefox-puppeteer (>=3.0.0,<5.0.0)", "marionette-client (>=2.2.0)", "mozfile (>=1.2)", "mozinfo (>=0.8)", "mozinstall (>=1.12)"]}], "summary": "Firefox UI Harness", "version": "1.3.0"}PKxHV+0firefox_ui_harness-1.3.0.dist-info/top_level.txtfirefox_ui_harness PKxH''\\(firefox_ui_harness-1.3.0.dist-info/WHEELWheel-Version: 1.0 Generator: bdist_wheel (0.26.0) Root-Is-Purelib: true Tag: py2-none-any PKxH=II+firefox_ui_harness-1.3.0.dist-info/METADATAMetadata-Version: 2.0 Name: firefox-ui-harness Version: 1.3.0 Summary: Firefox UI Harness Home-page: https://wiki.mozilla.org/Auto-tools/Projects/Marionette/Harnesses/FirefoxUI Author: Auto-tools Author-email: tools-marionette@lists.mozilla.org License: MPL Keywords: mozilla Platform: UNKNOWN Requires-Dist: firefox-puppeteer (>=3.0.0,<5.0.0) Requires-Dist: marionette-client (>=2.2.0) Requires-Dist: mozfile (>=1.2) Requires-Dist: mozinfo (>=0.8) Requires-Dist: mozinstall (>=1.12) Custom Marionette runner classes and entry scripts for Firefox Desktop specific Marionette tests. PKxHuE77)firefox_ui_harness-1.3.0.dist-info/RECORDfirefox_ui_harness/__init__.py,sha256=P_2jJaLfXSLYdbCnNIbhIsFtDmgVJ78SmMOo2YugDDk,263 firefox_ui_harness/cli_functional.py,sha256=nav4qA5hO4G0JCx6opO_w8ZAKrHaqaYKDWD7Ako421Y,574 firefox_ui_harness/cli_update.py,sha256=IN9t8FQ-NeqEsAJwXdzLID_V6el4bqlWfTaxyQAwHQk,562 firefox_ui_harness/testcases.py,sha256=PFE7zLCAbT0yLdWb1z02JcJQ8J9WICZE7GcYXhz3ihI,19167 firefox_ui_harness/arguments/__init__.py,sha256=PZVENI0nWqHobctzI4g17cuiEBqvbCmdChO2OQplV80,329 firefox_ui_harness/arguments/base.py,sha256=0MoTm5GmzAtvKUBXIx1Lb-Gs-bdhr1oW0fzGJ8O9ThU,544 firefox_ui_harness/arguments/update.py,sha256=1xUM0myDb3C8If-JqtaZ42o8k12iunQHX7grxkaXCbU,2333 firefox_ui_harness/runners/__init__.py,sha256=f6RfTxCqGdfQnMLePS4ZCr9MeKLM_8rPGyBZqHI_tqI,327 firefox_ui_harness/runners/base.py,sha256=qu6cP9fmUk7-Asl7owRSKPIRMYeEKEprzWDdA8POnoQ,1440 firefox_ui_harness/runners/update.py,sha256=4T0NVgKPXPNehi2xFniTW69tDYI39dZDUDbniPSzqGk,3372 firefox_ui_harness-1.3.0.dist-info/DESCRIPTION.rst,sha256=chKUDw_wv8fcDNyWO-8jdwmPG3r-n1pkAsNZu25HaPk,100 firefox_ui_harness-1.3.0.dist-info/METADATA,sha256=JXJZSCaHIwekWMSYkejCEDGjr7tiZcQ5w_5BqgIDhdU,585 firefox_ui_harness-1.3.0.dist-info/RECORD,, firefox_ui_harness-1.3.0.dist-info/WHEEL,sha256=JTb7YztR8fkPg6aSjc571Q4eiVHCwmUDlX8PhuuqIIE,92 firefox_ui_harness-1.3.0.dist-info/entry_points.txt,sha256=lFzusV5P6WJWe3xvNv-QpEUMbXYPhGXbHfrNaVA4MTY,165 firefox_ui_harness-1.3.0.dist-info/metadata.json,sha256=L3vJQih5hAd3HPUcUAeEt81IHCvBd2kC2td2tRpLiLY,994 firefox_ui_harness-1.3.0.dist-info/top_level.txt,sha256=XGfhts3QBZlysTXrs9ZFiUVupJ-gHx8ClA5r5z8tH_4,19 PKxHLfirefox_ui_harness/__init__.pyPK›xH22 Cfirefox_ui_harness/cli_update.pyPK›xHV>>$firefox_ui_harness/cli_functional.pyPKxHMJJ3firefox_ui_harness/testcases.pyPK4H^  &OQfirefox_ui_harness/arguments/update.pyPK4HD~II(Zfirefox_ui_harness/arguments/__init__.pyPK›xHe  $?\firefox_ui_harness/arguments/base.pyPKxHGmp, , $^firefox_ui_harness/runners/update.pyPKuuH^tGG&lfirefox_ui_harness/runners/__init__.pyPKxHV "mfirefox_ui_harness/runners/base.pyPKxH-dd2zsfirefox_ui_harness-1.3.0.dist-info/DESCRIPTION.rstPKxHA/3.tfirefox_ui_harness-1.3.0.dist-info/entry_points.txtPKxHns0$ufirefox_ui_harness-1.3.0.dist-info/metadata.jsonPKxHV+0Tyfirefox_ui_harness-1.3.0.dist-info/top_level.txtPKxH''\\(yfirefox_ui_harness-1.3.0.dist-info/WHEELPKxH=II+Wzfirefox_ui_harness-1.3.0.dist-info/METADATAPKxHuE77)|firefox_ui_harness-1.3.0.dist-info/RECORDPKg