#!/usr/bin/env python # ***** BEGIN LICENSE BLOCK ***** # 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/. # ***** END LICENSE BLOCK ***** import copy import os import pprint import sys import urllib # load modules from parent dir sys.path.insert(1, os.path.dirname(os.path.dirname(sys.path[0]))) from mozharness.base.python import PreScriptAction from mozharness.mozilla.buildbot import TBPL_SUCCESS, TBPL_WARNING, EXIT_STATUS_DICT from mozharness.mozilla.testing.firefox_ui_tests import ( FirefoxUIUpdateTests, firefox_ui_update_config_options ) # Command line arguments for release update tests firefox_ui_update_release_config_options = [ [['--build-number'], { 'dest': 'build_number', 'help': 'Build number of release, eg: 2', }], [['--limit-locales'], { 'dest': 'limit_locales', 'default': -1, 'type': int, 'help': 'Limit the number of locales to run.', }], [['--release-update-config'], { 'dest': 'release_update_config', 'help': 'Name of the release update verification config file to use.', }], [['--this-chunk'], { 'dest': 'this_chunk', 'default': 1, 'help': 'What chunk of locales to process.', }], [['--tools-repo'], { 'dest': 'tools_repo', 'default': 'http://hg.mozilla.org/build/tools', 'help': 'Which tools repo to check out', }], [['--tools-tag'], { 'dest': 'tools_tag', 'help': 'Which revision/tag to use for the tools repository.', }], [['--total-chunks'], { 'dest': 'total_chunks', 'default': 1, 'help': 'Total chunks to dive the locales into.', }], ] + copy.deepcopy(firefox_ui_update_config_options) class ReleaseFirefoxUIUpdateTests(FirefoxUIUpdateTests): def __init__(self): all_actions = [ 'clobber', 'checkout', 'create-virtualenv', 'query_minidump_stackwalk', 'read-release-update-config', 'run-tests', ] super(ReleaseFirefoxUIUpdateTests, self).__init__( all_actions=all_actions, default_actions=all_actions, config_options=firefox_ui_update_release_config_options, append_env_variables_from_configs=True, ) self.tools_repo = self.config.get('tools_repo') self.tools_tag = self.config.get('tools_tag') assert self.tools_repo and self.tools_tag, \ 'Without the "--tools-tag" we can\'t clone the releng\'s tools repository.' self.limit_locales = int(self.config.get('limit_locales')) # This will be a list containing one item per release based on configs # from tools/release/updates/*cfg self.releases = None def checkout(self): """ We checkout the tools repository and update to the right branch for it. """ dirs = self.query_abs_dirs() super(ReleaseFirefoxUIUpdateTests, self).checkout() self.vcs_checkout( repo=self.tools_repo, dest=dirs['abs_tools_dir'], branch=self.tools_tag, vcs='hg' ) def query_abs_dirs(self): if self.abs_dirs: return self.abs_dirs abs_dirs = super(ReleaseFirefoxUIUpdateTests, self).query_abs_dirs() dirs = { 'abs_tools_dir': os.path.join(abs_dirs['abs_work_dir'], 'tools'), } for key in dirs: if key not in abs_dirs: abs_dirs[key] = dirs[key] self.abs_dirs = abs_dirs return self.abs_dirs def read_release_update_config(self): ''' Builds a testing matrix based on an update verification configuration file under the tools repository (release/updates/*.cfg). Each release info line of the update verification files look similar to the following. NOTE: This shows each pair of information as a new line but in reality there is one white space separting them. We only show the values we care for. release="38.0" platform="Linux_x86_64-gcc3" build_id="20150429135941" locales="ach af ... zh-TW" channel="beta-localtest" from="/firefox/releases/38.0b9/linux-x86_64/%locale%/firefox-38.0b9.tar.bz2" ftp_server_from="http://archive.mozilla.org/pub" We will store this information in self.releases as a list of releases. NOTE: We will talk of full and quick releases. Full release info normally contains a subset of all locales (except for the most recent releases). A quick release has all locales, however, it misses the fields 'from' and 'ftp_server_from'. Both pairs of information complement each other but differ in such manner. ''' dirs = self.query_abs_dirs() assert os.path.exists(dirs['abs_tools_dir']), \ 'Without the tools/ checkout we can\'t use releng\'s config parser.' if self.config.get('release_update_config'): # The config file is part of the tools repository. Make sure that if specified # we force a revision of that repository to be set. if self.tools_tag is None: self.fatal('Make sure to specify the --tools-tag') self.release_update_config = self.config['release_update_config'] # Import the config parser sys.path.insert(1, os.path.join(dirs['abs_tools_dir'], 'lib', 'python')) from release.updates.verify import UpdateVerifyConfig uvc = UpdateVerifyConfig() config_file = os.path.join(dirs['abs_tools_dir'], 'release', 'updates', self.config['release_update_config']) uvc.read(config_file) if not hasattr(self, 'update_channel'): self.update_channel = uvc.channel # Filter out any releases that are less than Gecko 38 uvc.releases = [r for r in uvc.releases if int(r['release'].split('.')[0]) >= 38] temp_releases = [] for rel_info in uvc.releases: # This is the full release info if 'from' in rel_info and rel_info['from'] is not None: # Let's find the associated quick release which contains the remaining locales # for all releases except for the most recent release which contain all locales quick_release = uvc.getRelease(build_id=rel_info['build_id'], from_path=None) if quick_release != {}: rel_info['locales'] = sorted(rel_info['locales'] + quick_release['locales']) temp_releases.append(rel_info) uvc.releases = temp_releases chunked_config = uvc.getChunk( chunks=int(self.config['total_chunks']), thisChunk=int(self.config['this_chunk']) ) self.releases = chunked_config.releases @PreScriptAction('run-tests') def _pre_run_tests(self, action): assert ('release_update_config' in self.config or self.installer_url or self.installer_path), \ 'Either specify --update-verify-config, --installer-url or --installer-path.' def run_tests(self): dirs = self.query_abs_dirs() # We don't want multiple outputs of the same environment information. To prevent # that, we can't make it an argument of run_command and have to print it on our own. self.info('Using env: {}'.format(pprint.pformat(self.query_env()))) results = {} locales_counter = 0 for rel_info in sorted(self.releases, key=lambda release: release['build_id']): build_id = rel_info['build_id'] results[build_id] = {} self.info('About to run {buildid} {path} - {num_locales} locales'.format( buildid=build_id, path=rel_info['from'], num_locales=len(rel_info['locales']) )) # Each locale gets a fresh port to avoid address in use errors in case of # tests that time out unexpectedly. marionette_port = 2827 for locale in rel_info['locales']: locales_counter += 1 self.info('Running {buildid} {locale}'.format(buildid=build_id, locale=locale)) if self.limit_locales > -1 and locales_counter > self.limit_locales: self.info('We have reached the limit of locales we were intending to run') break if self.config['dry_run']: continue # Determine from where to download the file installer_url = '{server}/{fragment}'.format( server=rel_info['ftp_server_from'], fragment=urllib.quote(rel_info['from'].replace('%locale%', locale)) ) installer_path = self.download_file( url=installer_url, parent_dir=dirs['abs_work_dir'] ) binary_path = self.install_app(app=self.config.get('application'), installer_path=installer_path) marionette_port += 1 retcode = self.run_test( binary_path=binary_path, env=self.query_env(avoid_host_env=True), marionette_port=marionette_port, ) self.uninstall_app() # Remove installer which is not needed anymore self.info('Removing {}'.format(installer_path)) os.remove(installer_path) if retcode: self.warning('FAIL: {} has failed.'.format(sys.argv[0])) base_cmd = 'python {command} --firefox-ui-branch {branch} ' \ '--release-update-config {config} --tools-tag {tag}'.format( command=sys.argv[0], branch=self.firefox_ui_branch, config=self.release_update_config, tag=self.tools_tag ) for config in self.config['config_files']: base_cmd += ' --cfg {}'.format(config) if self.symbols_url: base_cmd += ' --symbols-path {}'.format(self.symbols_url) base_cmd += ' --installer-url {}'.format(installer_url) self.info('You can run the *specific* locale on the same machine with:') self.info(base_cmd) self.info('You can run the *specific* locale on *your* machine with:') self.info('{} --cfg developer_config.py'.format(base_cmd)) results[build_id][locale] = retcode self.info('Completed {buildid} {locale} with return code: {retcode}'.format( buildid=build_id, locale=locale, retcode=retcode)) if self.limit_locales > -1 and locales_counter > self.limit_locales: break # Determine which locales have failed and set scripts exit code exit_status = TBPL_SUCCESS for build_id in sorted(results.keys()): failed_locales = [] for locale in sorted(results[build_id].keys()): if results[build_id][locale] != 0: failed_locales.append(locale) if failed_locales: if exit_status == TBPL_SUCCESS: self.info('\nSUMMARY - Failed locales for {}:'.format(self.cli_script)) self.info('====================================================') exit_status = TBPL_WARNING self.info(build_id) self.info(' {}'.format(', '.join(failed_locales))) self.return_code = EXIT_STATUS_DICT[exit_status] if __name__ == '__main__': myScript = ReleaseFirefoxUIUpdateTests() myScript.run_and_exit()