Example #1
0
 def __init__(self, worker_num, ipaddr, tests, phone_cfg, user_cfg,
              autophone_queue, logfile_prefix, loglevel, mailer,
              build_cache_port):
     self.phone_cfg = phone_cfg
     self.user_cfg = user_cfg
     self.worker_num = worker_num
     self.ipaddr = ipaddr
     self.last_status_msg = None
     self.first_status_of_type = None
     self.last_status_of_previous_type = None
     self.crashes = Crashes(crash_window=user_cfg[PHONE_CRASH_WINDOW],
                            crash_limit=user_cfg[PHONE_CRASH_LIMIT])
     self.cmd_queue = multiprocessing.Queue()
     self.lock = multiprocessing.Lock()
     self.subprocess = PhoneWorkerSubProcess(self.worker_num, self.ipaddr,
                                             tests, phone_cfg, user_cfg,
                                             autophone_queue,
                                             self.cmd_queue, logfile_prefix,
                                             loglevel, mailer,
                                             build_cache_port)
     self.logger = logging.getLogger('autophone.worker')
     self.loggerdeco = LogDecorator(self.logger, {
         'phoneid': self.phone_cfg['phoneid'],
         'phoneip': self.phone_cfg['ip']
     }, '%(phoneid)s|%(phoneip)s|%(message)s')
Example #2
0
 def __init__(self, worker_num, ipaddr, tests, phone_cfg, user_cfg,
              autophone_queue, cmd_queue, logfile_prefix, loglevel, mailer,
              build_cache_port):
     self.worker_num = worker_num
     self.ipaddr = ipaddr
     self.tests = tests
     self.phone_cfg = phone_cfg
     self.user_cfg = user_cfg
     self.autophone_queue = autophone_queue
     self.cmd_queue = cmd_queue
     self.logfile = logfile_prefix + '.log'
     self.outfile = logfile_prefix + '.out'
     self.loglevel = loglevel
     self.mailer = mailer
     self.build_cache_port = build_cache_port
     self._stop = False
     self.p = None
     self.jobs = jobs.Jobs(self.mailer, self.phone_cfg['phoneid'])
     self.current_build = None
     self.last_ping = None
     self._dm = None
     self.status = None
     self.logger = logging.getLogger('autophone.worker.subprocess')
     self.loggerdeco = LogDecorator(self.logger, {
         'phoneid': self.phone_cfg['phoneid'],
         'phoneip': self.phone_cfg['ip']
     }, '%(phoneid)s|%(phoneip)s|%(message)s')
Example #3
0
    def setup_job(self):
        # Truncate the current log to prevent log bleed over. See the
        # main process log for all of the mising bits. Log the current
        # full contents of logcat, then clear the logcat buffers to
        # help prevent the device's buffer from over flowing during
        # the test.
        self.worker_subprocess.filehandler.stream.truncate(0)
        self.worker_subprocess.log_step('Setup Test')
        self.start_time = datetime.datetime.utcnow()
        self.stop_time = self.start_time
        # Clear the Treeherder job details.
        self.job_details = []
        try:
            self.worker_subprocess.logcat.reset()
        except:
            self.loggerdeco.exception('Exception resetting logcat before test')
        self.worker_subprocess.treeherder.submit_running(
            self.phone.id,
            self.build.url,
            self.build.tree,
            self.build.revision,
            self.build.type,
            self.build.abi,
            self.build.platform,
            self.build.sdk,
            self.build.builder_type,
            tests=[self])

        self.loggerdeco_original = self.loggerdeco
        # self.dm._logger can raise ADBTimeoutError due to the
        # property dm therefore place it after the initialization.
        self.dm_logger_original = self.dm._logger
        logger = utils.getLogger()
        self.loggerdeco = LogDecorator(logger,
                                       {
                                           'repo': self.build.tree,
                                           'buildid': self.build.id,
                                           'buildtype': self.build.type,
                                           'sdk': self.phone.sdk,
                                           'platform': self.build.platform,
                                           'testname': self.name
                                       },
                                       'PhoneTestJob %(repo)s %(buildid)s %(buildtype)s %(sdk)s %(platform)s %(testname)s '
                                       '%(message)s')
        self.dm._logger = self.loggerdeco
        self.loggerdeco.info('PhoneTest starting job')
        if self.unittest_logpath:
            os.unlink(self.unittest_logpath)
        self.unittest_logpath = None
        self.upload_dir = tempfile.mkdtemp()
        self.crash_processor = AutophoneCrashProcessor(self.dm,
                                                       self.profile_path,
                                                       self.upload_dir,
                                                       self.build.app_name)
        self.crash_processor.clear()
        self.reset_result()
        if not self.worker_subprocess.is_disabled():
            self.update_status(phone_status=PhoneStatus.WORKING,
                               message='Setting up %s' % self.name)
Example #4
0
    def setup_job(self):
        # Log the current full contents of logcat, then clear the
        # logcat buffers to help prevent the device's buffer from
        # over flowing during the test.
        self.loggerdeco.debug('PhoneTest.teardown_job')
        self.start_time = datetime.datetime.utcnow()
        self.stop_time = self.start_time
        # Clear the Treeherder job details.
        self.job_details = []
        self.loggerdeco.debug('full logcat before job:')
        try:
            self.loggerdeco.debug('\n'.join(
                self.worker_subprocess.logcat.get(full=True)))
        except:
            self.loggerdeco.exception('Exception getting logcat')
        try:
            self.worker_subprocess.logcat.reset()
        except:
            self.loggerdeco.exception('Exception resetting logcat')
        self.worker_subprocess.treeherder.submit_running(
            self.phone.id,
            self.build.url,
            self.build.tree,
            self.build.revision,
            self.build.type,
            self.build.abi,
            self.build.platform,
            self.build.sdk,
            self.build.builder_type,
            tests=[self])

        self.loggerdeco_original = self.loggerdeco
        # self.dm._logger can raise ADBTimeoutError due to the
        # property dm therefore place it after the initialization.
        self.dm_logger_original = self.dm._logger
        logger = logging.getLogger()
        self.loggerdeco = LogDecorator(
            logger, {
                'phoneid': self.phone.id,
                'buildid': self.build.id,
                'test': self.name
            }, 'PhoneTestJob|%(phoneid)s|%(buildid)s|%(test)s|'
            '%(message)s')
        self.dm._logger = self.loggerdeco
        self.loggerdeco.info('PhoneTest starting job')
        if self.unittest_logpath:
            os.unlink(self.unittest_logpath)
        self.unittest_logpath = None
        self.upload_dir = tempfile.mkdtemp()
        self.crash_processor = AutophoneCrashProcessor(self.dm,
                                                       self.profile_path,
                                                       self.upload_dir,
                                                       self.build.app_name)
        self.crash_processor.clear()
        self.reset_result()
        if not self.worker_subprocess.is_disabled():
            self.update_status(phone_status=PhoneStatus.WORKING,
                               message='Setting up %s' % self.name)
Example #5
0
    def set_worker_subprocess(self, worker_subprocess):
        logger = utils.getLogger()
        self.loggerdeco = LogDecorator(logger,
                                       {},
                                       '%(message)s')

        self.loggerdeco_original = None
        self.dm_logger_original = None
        self.worker_subprocess = worker_subprocess
        self.dm = worker_subprocess.dm
        self.update_status_cb = worker_subprocess.update_status
Example #6
0
    def set_worker_subprocess(self, worker_subprocess):
        logger = logging.getLogger()
        self.loggerdeco = LogDecorator(
            logger, {'phoneid': self.phone.id},
            'PhoneTestSubProcess|%(phoneid)s|%(message)s')

        self.loggerdeco.info('PhoneTest.set_worker_subprocess')

        self.loggerdeco_original = None
        self.dm_logger_original = None
        self.worker_subprocess = worker_subprocess
        self.dm = worker_subprocess.dm
        self.update_status_cb = worker_subprocess.update_status
Example #7
0
    def runtest(self):

        self.loggerdeco = LogDecorator(self.logger,
                                       {'phoneid': self.phone.id,
                                        'buildid': self.parms['buildid'],
                                        'testname': self.parms['test_name']},
                                       '%(phoneid)s|%(buildid)s|'
                                       '%(testname)s|%(message)s')

        if self.logger.getEffectiveLevel() == logging.DEBUG:
            self.loggerdeco.debug('runtestsremote.py runtest start')
            for key in self.parms.keys():
                self.loggerdeco.debug('test parameters: %s = %s' %
                                      (key, self.parms[key]))

        self.update_status(message='Starting test %s' % self.parms['test_name'])

        if self.parms['test_name'] == 'robocoptest':
            try:
                self.dm.uninstall_app('org.mozilla.roboexample.test')
                robocop_apk_path = os.path.join(self.parms['build_dir'], 'robocop.apk')
                self.dm.install_app(robocop_apk_path)
            except Exception, e:
                self.loggerdeco.exception('runtestsremote.py:runtest: Exception running test.')
                self.test_result.status = PhoneTestResult.EXCEPTION
                self.message = 'Exception installing robocop.apk: %s' % e
                with open(self._log, "w") as logfilehandle:
                    logfilehandle.write('%s\n' % self.message)
                return
Example #8
0
    def setup_job(self):
        self.logger_original = self.logger
        self.loggerdeco_original = self.loggerdeco
        self.dm_logger_original = self.dm._logger
        # Clear the log file if we are submitting logs to Treeherder.
        if (self.worker_subprocess.options.treeherder_url and
            self.worker_subprocess.build.revision_hash and
            self.worker_subprocess.s3_bucket):
            self.worker_subprocess.initialize_log_filehandler()
        self.worker_subprocess.treeherder.submit_running(tests=[self])

        self.logger = logging.getLogger('autophone.worker.subprocess.test')
        self.loggerdeco = LogDecorator(self.logger,
                                       {'phoneid': self.phone.id,
                                        'pid': os.getpid(),
                                        'buildid': self.build.id,
                                        'test': self.name},
                                       '%(phoneid)s|%(pid)s|%(buildid)s|%(test)s|'
                                       '%(message)s')
        self.dm._logger = self.loggerdeco
        self.loggerdeco.debug('PhoneTest.setup_job')
        if self._log:
            os.unlink(self._log)
        self._log = None
        self.upload_dir = tempfile.mkdtemp()
        self.test_result = PhoneTestResult()
Example #9
0
    def setup_job(self):
        self.start_time = datetime.datetime.now()
        self.stop_time = self.start_time
        # Clear the Treeherder job details.
        self.job_details = []
        self.worker_subprocess.treeherder.submit_running(
            self.phone.id,
            self.build.url,
            self.build.tree,
            self.build.revision_hash,
            tests=[self])

        self.loggerdeco_original = self.loggerdeco
        # self.dm._logger can raise ADBTimeoutError due to the
        # property dm therefore place it after the initialization.
        self.dm_logger_original = self.dm._logger
        self.loggerdeco = LogDecorator(logger,
                                       {'phoneid': self.phone.id,
                                        'buildid': self.build.id,
                                        'test': self.name},
                                       '%(phoneid)s|%(buildid)s|%(test)s|'
                                       '%(message)s')
        self.dm._logger = self.loggerdeco
        self.loggerdeco.debug('PhoneTest.setup_job')
        if self._log:
            os.unlink(self._log)
        self._log = None
        self.upload_dir = tempfile.mkdtemp()
        self.test_result = PhoneTestResult()
        if not self.worker_subprocess.is_disabled():
            self.update_status(phone_status=PhoneStatus.WORKING,
                               message='Setting up %s' % self.name)
Example #10
0
 def runjob(self, build_metadata, worker_subprocess):
     logger = self.logger
     loggerdeco = self.loggerdeco
     self.logger = logging.getLogger('autophone.worker.subprocess.test')
     self.loggerdeco = LogDecorator(
         self.logger, {
             'phoneid': self.phone_cfg['phoneid'],
             'phoneip': self.phone_cfg['ip'],
             'buildid': build_metadata['buildid']
         }, '%(phoneid)s|%(phoneip)s|%(buildid)s|'
         '%(message)s')
     try:
         self.setup_and_runtest(build_metadata, worker_subprocess)
     finally:
         self.logger = logger
         self.loggerdeco = loggerdeco
Example #11
0
 def __init__(self, dm=None, phone=None, options=None, config_file=None, chunk=1, repos=[]):
     # Ensure that repos is a list and that it is sorted in order
     # for comparisons with the tests loaded from the jobs database
     # to work.
     assert type(repos) == list, 'PhoneTest repos argument must be a list'
     repos.sort()
     self._add_instance(phone.id, config_file, chunk)
     self.config_file = config_file
     self.cfg = ConfigParser.ConfigParser()
     self.cfg.read(self.config_file)
     self.enable_unittests = False
     self.chunk = chunk
     self.chunks = 1
     self.update_status_cb = None
     self.dm = dm
     self.phone = phone
     self.worker_subprocess = None
     self.options = options
     self.loggerdeco = LogDecorator(logger,
                                    {'phoneid': self.phone.id},
                                    '%(phoneid)s|%(message)s')
     self.loggerdeco_original = None
     self.dm_logger_original = None
     self.loggerdeco.info('init autophone.phonetest')
     self._base_device_path = ''
     self.profile_path = '/data/local/tmp/profile'
     self.repos = repos
     self._log = None
     # Treeherder related items.
     self._job_name = None
     self._job_symbol = None
     self._group_name = None
     self._group_symbol = None
     self.test_result = PhoneTestResult()
     self.message = None
     # A unique consistent guid is necessary for identifying the
     # test job in treeherder. The test job_guid is updated when a
     # test is added to the pending jobs/tests in the jobs
     # database.
     self.job_guid = None
     self.job_details = []
     self.submit_timestamp = None
     self.start_timestamp = None
     self.end_timestamp = None
     self.logcat = Logcat(self)
     self.loggerdeco.debug('PhoneTest: %s, cfg sections: %s' % (self.__dict__, self.cfg.sections()))
     if not self.cfg.sections():
         self.loggerdeco.warning('Test configuration not found. '
                                 'Will use defaults.')
     # upload_dir will hold ANR traces, tombstones and other files
     # pulled from the device.
     self.upload_dir = None
     # crash_processor is an instance of AutophoneCrashProcessor that
     # is used by non-unittests to process device errors and crashes.
     self.crash_processor = None
     # Instrument running time
     self.start_time = None
     self.stop_time = None
     self.loggerdeco.info('PhoneTest: Connected.')
Example #12
0
 def runjob(self, build_metadata, worker_subprocess):
     logger = self.logger
     loggerdeco = self.loggerdeco
     self.logger = logging.getLogger('autophone.worker.subprocess.test')
     self.loggerdeco = LogDecorator(self.logger,
                                    {'phoneid': self.phone_cfg['phoneid'],
                                     'buildid': build_metadata['buildid']},
                                    '%(phoneid)s|%(buildid)s|%(message)s')
     try:
         self.setup_and_runtest(build_metadata, worker_subprocess)
     finally:
         self.logger = logger
         self.loggerdeco = loggerdeco
Example #13
0
 def __init__(self, phone, options, config_file=None,
              enable_unittests=False, test_devices_repos={},
              chunk=1):
     self.config_file = config_file
     self.cfg = ConfigParser.RawConfigParser()
     self.cfg.read(self.config_file)
     self.enable_unittests = enable_unittests
     self.test_devices_repos = test_devices_repos
     self.chunk = chunk
     self.chunks = 1
     self.update_status_cb = None
     self.phone = phone
     self.worker_subprocess = None
     self.options = options
     self.logger = logging.getLogger('autophone.phonetest')
     self.loggerdeco = LogDecorator(self.logger,
                                    {'phoneid': self.phone.id,
                                     'pid': os.getpid()},
                                    '%(phoneid)s|%(pid)s|%(message)s')
     self.logger_original = None
     self.loggerdeco_original = None
     self.dm_logger_original = None
     self.loggerdeco.info('init autophone.phonetest')
     self._base_device_path = ''
     self.profile_path = '/data/local/tmp/profile'
     self._dm = None
     self._repos = None
     self._log = None
     # Treeherder related items.
     self._job_name = None
     self._job_symbol = None
     self._group_name = None
     self._group_symbol = None
     self.test_result = PhoneTestResult()
     self.message = None
     self.job_guid = None
     self.job_details = []
     self.submit_timestamp = None
     self.start_timestamp = None
     self.end_timestamp = None
     self.logcat = Logcat(self)
     self.loggerdeco.debug('PhoneTest: %s, cfg sections: %s' % (self.__dict__, self.cfg.sections()))
     if not self.cfg.sections():
         self.loggerdeco.warning('Test configuration not found. '
                                 'Will use defaults.')
     # upload_dir will hold ANR traces, tombstones and other files
     # pulled from the device.
     self.upload_dir = None
     # crash_processor is an instance of AutophoneCrashProcessor that
     # is used by non-unittests to process device errors and crashes.
     self.crash_processor = None
Example #14
0
    def setup_job(self):
        # Log the current full contents of logcat, then clear the
        # logcat buffers to help prevent the device's buffer from
        # over flowing during the test.
        self.start_time = datetime.datetime.now()
        self.stop_time = self.start_time
        # Clear the Treeherder job details.
        self.job_details = []
        self.loggerdeco.debug('phonetest.setup_job: full logcat before job:')
        try:
            self.loggerdeco.debug('\n'.join(self.logcat.get(full=True)))
        except:
            self.loggerdeco.exception('Exception getting logcat')
        try:
            self.logcat.reset()
        except:
            self.loggerdeco.exception('Exception resetting logcat')
        self.worker_subprocess.treeherder.submit_running(
            self.phone.id,
            self.build.url,
            self.build.tree,
            self.build.revision_hash,
            tests=[self])

        self.loggerdeco_original = self.loggerdeco
        # self.dm._logger can raise ADBTimeoutError due to the
        # property dm therefore place it after the initialization.
        self.dm_logger_original = self.dm._logger
        # Create a test run specific logger which will propagate to
        # the root logger in the worker which runs in the same
        # process. This log will be uploaded to Treeherder if
        # Treeherder submission is enabled and will be cleared at the
        # beginning of each test run.
        sensitive_data_filter = SensitiveDataFilter(self.options.sensitive_data)
        logger = logging.getLogger('phonetest')
        logger.addFilter(sensitive_data_filter)
        logger.propagate = True
        logger.setLevel(self.worker_subprocess.loglevel)
        self.test_logfile = (self.worker_subprocess.logfile_prefix +
                             '-' + self.name + '.log')
        self.test_logfilehandler = logging.FileHandler(
            self.test_logfile, mode='w')
        fileformatstring = ('%(asctime)s|%(process)d|%(threadName)s|%(name)s|'
                            '%(levelname)s|%(message)s')
        fileformatter = logging.Formatter(fileformatstring)
        self.test_logfilehandler.setFormatter(fileformatter)
        logger.addHandler(self.test_logfilehandler)

        self.loggerdeco = LogDecorator(logger,
                                       {'phoneid': self.phone.id,
                                        'buildid': self.build.id,
                                        'test': self.name},
                                       '%(phoneid)s|%(buildid)s|%(test)s|'
                                       '%(message)s')
        self.dm._logger = self.loggerdeco
        self.loggerdeco.debug('PhoneTest.setup_job')
        if self.unittest_logpath:
            os.unlink(self.unittest_logpath)
        self.unittest_logpath = None
        self.upload_dir = tempfile.mkdtemp()
        self.crash_processor = AutophoneCrashProcessor(self.dm,
                                                       self.profile_path,
                                                       self.upload_dir,
                                                       self.build.app_name)
        self.crash_processor.clear()
        self.test_result = PhoneTestResult()
        if not self.worker_subprocess.is_disabled():
            self.update_status(phone_status=PhoneStatus.WORKING,
                               message='Setting up %s' % self.name)
Example #15
0
class S1S2Test(PerfTest):
    def __init__(self, dm=None, phone=None, options=None,
                 config_file=None, chunk=1, repos=[]):
        PerfTest.__init__(self, dm=dm, phone=phone, options=options,
                          config_file=config_file, chunk=chunk, repos=repos)

        # [paths]
        autophone_directory = os.path.dirname(os.path.abspath(sys.argv[0]))
        self._paths = {}
        self._paths['dest'] = posixpath.join(self.base_device_path, 's1s2test/')
        try:
            sources = self.cfg.get('paths', 'sources').split()
            self._paths['sources'] = []
            for source in sources:
                if not source.endswith('/'):
                    source += '/'
                self._paths['sources'].append(source)
        except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
            self._paths['sources'] = [
                os.path.join(autophone_directory, 'files/base/'),
                os.path.join(autophone_directory, 'files/s1s2/')]
        try:
            self._paths['dest'] = self.cfg.get('paths', 'dest')
            if not self._paths['dest'].endswith('/'):
                self._paths['dest'] += '/'
        except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
            pass
        try:
            self._paths['profile'] = self.cfg.get('paths', 'profile')
            if not self._paths['profile'].endswith('/'):
                self._paths['profile'] += '/'
        except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
            pass
        if 'profile' in self._paths:
            self.profile_path = self._paths['profile']
        # _pushes = {'sourcepath' : 'destpath', ...}
        self._pushes = {}
        for source in self._paths['sources']:
            for push in glob.glob(source + '*'):
                if push.endswith('~') or push.endswith('.bak'):
                    continue
                push_dest = posixpath.join(self._paths['dest'], os.path.basename(push))
                self._pushes[push] = push_dest
        # [tests]
        self._tests = {}
        try:
            for t in self.cfg.items('tests'):
                self._tests[t[0]] = t[1]
        except ConfigParser.NoSectionError:
            self._tests['blank'] = 'blank.html'
        # Map URLS - {urlname: url} - urlname serves as testname
        self._urls = {}
        config_vars = {'webserver_url': options.webserver_url}
        try:
            location_items = self.cfg.items('locations', False, config_vars)
        except ConfigParser.NoSectionError:
            location_items = [('local', None)]
        for test_location, test_path in location_items:
            if test_location in config_vars:
                # Ignore the pseudo-options which result from passing
                # the config_vars for interpolation.
                continue
            for test_name in self._tests:
                if test_path:
                    test_url = urlparse.urljoin(test_path, self._tests[test_name])
                else:
                    test_url = os.path.join('file://', self._paths['dest'],
                                            self._tests[test_name])
                self.loggerdeco.debug(
                    'test_location: %s, test_name: %s, test_path: %s, '
                    'test: %s, test_url: %s' %
                    (test_location, test_name, test_path,
                     self._tests[test_name], test_url))
                self._urls["%s-%s" % (test_location, test_name)] = test_url
        self._initialize_url = os.path.join('file://', self._paths['dest'],
                                            'initialize_profile.html')

    @property
    def name(self):
        return 'autophone-s1s2%s' % self.name_suffix

    @property
    def phonedash_url(self):
        # For s1s2test, there are 4 different test names due to historical design.
        # We pick local-blank as the default.
        return self._phonedash_url('local-blank')

    def setup_job(self):
        PerfTest.setup_job(self)

    def run_job(self):
        if not self.install_local_pages():
            self.test_failure(
                self.name, 'TEST_UNEXPECTED_FAIL',
                'Aborting test - Could not install local pages on phone.',
                PhoneTestResult.EXCEPTION)
            return

        if not self.create_profile():
            self.test_failure(
                self.name, 'TEST_UNEXPECTED_FAIL',
                'Aborting test - Could not run Fennec.',
                PhoneTestResult.BUSTED)
            return

        testcount = len(self._urls.keys())
        for testnum,(testname,url) in enumerate(self._urls.iteritems(), 1):
            if self.fennec_crashed:
                break
            self.loggerdeco = LogDecorator(logger,
                                           {'phoneid': self.phone.id,
                                            'buildid': self.build.id,
                                            'testname': testname},
                                           '%(phoneid)s|%(buildid)s|'
                                           '%(testname)s|%(message)s')
            self.dm._logger = self.loggerdeco
            self.loggerdeco.info('Running test (%d/%d) for %d iterations' %
                                 (testnum, testcount, self._iterations))

            # success == False indicates that none of the attempts
            # were successful in getting any measurement. This is
            # typically due to a regression in the brower which should
            # be reported.
            success = False
            command = None
            for attempt in range(1, self.stderrp_attempts+1):
                if self.fennec_crashed:
                    break
                # dataset is a list of the measurements made for the
                # iterations for this test.
                #
                # An empty item in the dataset list represents a
                # failure to obtain any measurement for that
                # iteration.
                #
                # It is possible for an item in the dataset to have an
                # uncached value and not have a corresponding cached
                # value if the cached test failed to record the
                # values.

                dataset = []
                for iteration in range(1, self._iterations+1):
                    command = self.worker_subprocess.process_autophone_cmd(self)
                    if command['interrupt']:
                        self.handle_test_interrupt(command['reason'])
                        break
                    if self.fennec_crashed:
                        break
                    self.update_status(message='Attempt %d/%d for Test %d/%d, '
                                       'run %d, for url %s' %
                                       (attempt, self.stderrp_attempts,
                                        testnum, testcount, iteration, url))

                    dataset.append({})

                    if not self.create_profile():
                        self.test_failure(url,
                                          'TEST_UNEXPECTED_FAIL',
                                          'Failed to create profile',
                                          PhoneTestResult.TESTFAILED)
                        continue

                    measurement = self.runtest(url)
                    if measurement:
                        self.test_pass(url)
                    else:
                        self.test_failure(
                            url,
                            'TEST_UNEXPECTED_FAIL',
                            'Failed to get uncached measurement.',
                            PhoneTestResult.TESTFAILED)
                        continue
                    dataset[-1]['uncached'] = measurement
                    success = True

                    measurement = self.runtest(url)
                    if measurement:
                        self.test_pass(url)
                    else:
                        self.test_failure(
                            url,
                            'TEST_UNEXPECTED_FAIL',
                            'Failed to get cached measurement.',
                            PhoneTestResult.TESTFAILED)
                        continue
                    dataset[-1]['cached'] = measurement

                    if self.is_stderr_below_threshold(
                            ('throbberstart',
                             'throbberstop'),
                            dataset,
                            self.stderrp_accept):
                        self.loggerdeco.info(
                            'Accepted test (%d/%d) after %d of %d iterations' %
                            (testnum, testcount, iteration, self._iterations))
                        break

                if command and command['interrupt']:
                    break
                if not success:
                    # If we have not gotten a single measurement at this point,
                    # just bail and report the failure rather than wasting time
                    # continuing more attempts.
                    self.loggerdeco.info(
                        'Failed to get measurements for test %s after %d/%d attempt '
                        'of %d iterations' % (testname, attempt,
                                              self.stderrp_attempts,
                                              self._iterations))
                    self.worker_subprocess.mailer.send(
                        'S1S2Test %s failed for Build %s %s on Phone %s' %
                        (testname, self.build.tree, self.build.id,
                         self.phone.id),
                        'No measurements were detected for test %s.\n\n'
                        'Repository: %s\n'
                        'Build Id:   %s\n'
                        'Revision:   %s\n' %
                        (testname, self.build.tree, self.build.id, self.build.revision))
                    self.test_failure(self.name, 'TEST_UNEXPECTED_FAIL',
                                      'No measurements detected.',
                                      PhoneTestResult.BUSTED)
                    break

                if self.is_stderr_below_threshold(
                        ('throbberstart',
                         'throbberstop'),
                        dataset,
                        self.stderrp_reject):
                    rejected = False
                else:
                    rejected = True
                    self.loggerdeco.info(
                        'Rejected test (%d/%d) after %d/%d iterations' %
                        (testnum, testcount, iteration, self._iterations))

                self.loggerdeco.debug('publishing results')

                for datapoint in dataset:
                    for cachekey in datapoint:
                        self.report_results(
                            starttime=datapoint[cachekey]['starttime'],
                            tstrt=datapoint[cachekey]['throbberstart'],
                            tstop=datapoint[cachekey]['throbberstop'],
                            testname=testname,
                            cache_enabled=(cachekey=='cached'),
                            rejected=rejected)
                if not rejected:
                    break

            if command and command['interrupt']:
                break
            elif not success:
                break

    def runtest(self, url):
        # Clear logcat
        self.logcat.clear()

        # Run test
        self.run_fennec_with_profile(self.build.app_name, url)

        # Get results - do this now so we don't have as much to
        # parse in logcat.
        starttime, throbberstart, throbberstop = self.analyze_logcat()

        self.wait_for_fennec()

        # Ensure we succeeded - no 0's reported
        datapoint = {}
        if (throbberstart and throbberstop):
            datapoint['starttime'] = starttime
            datapoint['throbberstart'] = throbberstart
            datapoint['throbberstop'] = throbberstop
            datapoint['throbbertime'] = throbberstop - throbberstart
        return datapoint

    def wait_for_fennec(self, max_wait_time=60, wait_time=5,
                        kill_wait_time=20, root=True):
        # Wait for up to a max_wait_time seconds for fennec to close
        # itself in response to the quitter request. Check that fennec
        # is still running every wait_time seconds. If fennec doesn't
        # close on its own, attempt up to 3 times to kill fennec, waiting
        # kill_wait_time seconds between attempts.
        # Return True if fennec exits on its own, False if it needs to be killed.
        # Re-raise the last exception if fennec can not be killed.
        max_wait_attempts = max_wait_time / wait_time
        for wait_attempt in range(1, max_wait_attempts+1):
            if not self.dm.process_exist(self.build.app_name):
                return True
            sleep(wait_time)
        self.loggerdeco.debug('killing fennec')
        max_killattempts = 3
        for kill_attempt in range(1, max_killattempts+1):
            try:
                self.dm.pkill(self.build.app_name, root=root)
                break
            except ADBError:
                self.loggerdeco.exception('Attempt %d to kill fennec failed' %
                                          kill_attempt)
                if kill_attempt == max_killattempts:
                    raise
                sleep(kill_wait_time)
        return False

    def create_profile(self, custom_prefs=None, root=True):
        # Create, install and initialize the profile to be
        # used in the test.

        # make sure firefox isn't running when we try to
        # install the profile.

        self.dm.pkill(self.build.app_name, root=root)

        telemetry_prompt = 999
        if self.build.id < '20130103':
            telemetry_prompt = 2
        prefs = {
            'app.update.auto': False,
            'app.update.enabled': False,
            'app.update.url': '',
            'app.update.url.android':  '',
            'app.update.url.override': '',
            'beacon.enabled': False,
            'browser.EULA.override': True,
            'browser.safebrowsing.enabled': False,
            'browser.safebrowsing.malware.enabled': False,
            'browser.search.countryCode': 'US',
            'browser.search.isUS': True,
            'browser.selfsupport.url': '',
            'browser.sessionstore.resume_from_crash': False,
            'browser.snippets.enabled': False,
            'browser.snippets.firstrunHomepage.enabled': False,
            'browser.snippets.syncPromo.enabled': False,
            'browser.warnOnQuit': False,
            'browser.webapps.checkForUpdates': 0,
            'datareporting.healthreport.service.enabled': False,
            'datareporting.policy.dataSubmissionPolicyBypassAcceptance': True,
            'dom.ipc.plugins.flash.subprocess.crashreporter.enabled': False,
            'extensions.blocklist.enabled': False,
            'extensions.getAddons.cache.enabled': False,
            'extensions.update.enabled': False,
            'general.useragent.updates.enabled': False,
            'media.autoplay.enabled': True,
            'shell.checkDefaultClient': False,
            'toolkit.telemetry.enabled': False,
            'toolkit.telemetry.notifiedOptOut': telemetry_prompt,
            'toolkit.telemetry.prompted': telemetry_prompt,
            'urlclassifier.updateinterval': 172800,
            'webapprt.app_update_interval': 86400,
            'xpinstall.signatures.required': False,
            }
        if isinstance(custom_prefs, dict):
            prefs = dict(prefs.items() + custom_prefs.items())
        profile = FirefoxProfile(preferences=prefs, addons='%s/xpi/quitter.xpi' %
                                 os.getcwd())
        if not self.install_profile(profile):
            return False

        success = False
        for attempt in range(1, self.options.phone_retry_limit+1):
            self.loggerdeco.debug('Attempt %d Initializing profile' % attempt)
            self.run_fennec_with_profile(self.build.app_name, self._initialize_url)
            if self.wait_for_fennec():
                success = True
                break
            sleep(self.options.phone_retry_wait)

        if not success:
            msg = 'Aborting Test - Failure initializing profile.'
            self.loggerdeco.error(msg)

        return success

    def install_local_pages(self):
        success = False
        for attempt in range(1, self.options.phone_retry_limit+1):
            self.loggerdeco.debug('Attempt %d Installing local pages' % attempt)
            try:
                self.dm.rm(self._paths['dest'], recursive=True, force=True)
                self.dm.mkdir(self._paths['dest'], parents=True)
                for push_source in self._pushes:
                    push_dest = self._pushes[push_source]
                    if os.path.isdir(push_source):
                        self.dm.push(push_source, push_dest)
                    else:
                        self.dm.push(push_source, push_dest)
                success = True
                break
            except ADBError:
                self.loggerdeco.exception('Attempt %d Installing local pages' % attempt)
                sleep(self.options.phone_retry_wait)

        if not success:
            self.loggerdeco.error('Failure installing local pages')

        return success

    def is_fennec_running(self, appname):
        for attempt in range(1, self.options.phone_retry_limit+1):
            try:
                return self.dm.process_exist(appname)
            except ADBError:
                self.loggerdeco.exception('Attempt %d is fennec running' % attempt)
                if attempt == self.options.phone_retry_limit:
                    raise
                sleep(self.options.phone_retry_wait)

    def analyze_logcat(self):
        self.loggerdeco.debug('analyzing logcat')

        logcat_prefix = '(\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3})'
        throbber_prefix = '..GeckoToolbarDisplayLayout.*zerdatime (\d+) - Throbber'
        re_base_time = re.compile('%s' % logcat_prefix)
        re_start_time = re.compile(
            '%s .*(Gecko|Start proc %s for activity %s/.App)' % (
                logcat_prefix, self.build.app_name, self.build.app_name))
        re_throbber_start_time = re.compile('%s %s start' %
                                            (logcat_prefix, throbber_prefix))
        re_throbber_stop_time = re.compile('%s %s stop' %
                                           (logcat_prefix, throbber_prefix))

        base_time = 0
        start_time = 0
        throbber_start_time = 0
        throbber_stop_time = 0

        attempt = 1
        max_time = 90 # maximum time to wait for throbbers
        wait_time = 3 # time to wait between attempts
        max_attempts = max_time / wait_time

        while (attempt <= max_attempts and (throbber_start_time == 0 or
                                            throbber_stop_time == 0)):
            buf = self.logcat.get()
            for line in buf:
                self.loggerdeco.debug('analyze_logcat: %s' % line)
                match = re_base_time.match(line)
                if match and not base_time:
                    base_time = match.group(1)
                    self.loggerdeco.debug('analyze_logcat: base_time: %s' % base_time)
                # we want the first throbberstart and throbberstop.
                match = re_start_time.match(line)
                if match and not start_time:
                    start_time = match.group(1)
                    self.loggerdeco.debug('analyze_logcat: start_time: %s' % start_time)
                    continue
                match = re_throbber_start_time.match(line)
                if match and not throbber_start_time:
                    throbber_start_time = match.group(1)
                    self.loggerdeco.debug('analyze_logcat: throbber_start_time: %s' % throbber_start_time)
                    continue
                match = re_throbber_stop_time.match(line)
                if match and not throbber_stop_time:
                    throbber_stop_time = match.group(1)
                    self.loggerdeco.debug('analyze_logcat: throbber_stop_time: %s' % throbber_stop_time)
                    continue
                if start_time and throbber_start_time and throbber_stop_time:
                    break
            if self.fennec_crashed:
                # If fennec crashed, don't bother looking for the Throbbers
                break
            if (start_time == 0 or
                throbber_start_time == 0 or
                throbber_stop_time == 0):
                sleep(wait_time)
                attempt += 1
        if throbber_start_time and throbber_stop_time == 0:
            self.loggerdeco.info('Unable to find Throbber stop')

        # The captured time from the logcat lines is in the format
        # MM-DD HH:MM:SS.mmm. It is possible for the year to change
        # between the different times, so we need to make adjustments
        # if necessary. First, we assume the year does not change and
        # parse the dates as if they are in the current year. If
        # the dates violate the natural order start_time,
        # throbber_start_time, throbber_stop_time, we can adjust the
        # year.

        if base_time and start_time and throbber_start_time and throbber_stop_time:
            parse = lambda y, t: datetime.datetime.strptime('%4d-%s' % (y, t), '%Y-%m-%d %H:%M:%S.%f')
            year = datetime.datetime.now().year
            base_time = parse(year, base_time)
            start_time = parse(year, start_time)
            throbber_start_time = parse(year, throbber_start_time)
            throbber_stop_time = parse(year, throbber_stop_time)

            self.loggerdeco.debug('analyze_logcat: before year adjustment '
                                  'base: %s, start: %s, '
                                  'throbber start: %s' %
                                  (base_time, start_time,
                                   throbber_start_time))

            if base_time > start_time:
                base_time.replace(year=year-1)
            elif start_time > throbber_start_time:
                base_time.replace(year=year-1)
                start_time.replace(year=year-1)
            elif throbber_start_time > throbber_stop_time:
                base_time.replace(year=year-1)
                start_time.replace(year=year-1)
                throbber_start_time.replace(year-1)

            self.loggerdeco.debug('analyze_logcat: after year adjustment '
                                  'base: %s, start: %s, '
                                  'throbber start: %s' %
                                  (base_time, start_time,
                                   throbber_start_time))

            # Convert the times to milliseconds from the base time.
            convert = lambda t1, t2: round((t2 - t1).total_seconds() * 1000.0)

            start_time = convert(base_time, start_time)
            throbber_start_time = convert(base_time, throbber_start_time)
            throbber_stop_time = convert(base_time, throbber_stop_time)

            self.loggerdeco.debug('analyze_logcat: base: %s, start: %s, '
                                  'throbber start: %s, throbber stop: %s, '
                                  'throbber time: %s ' %
                                  (base_time, start_time,
                                   throbber_start_time, throbber_stop_time,
                                   throbber_stop_time - throbber_start_time))

            if (start_time > throbber_start_time or
                start_time > throbber_stop_time or
                throbber_start_time > throbber_stop_time):
                self.loggerdeco.warning('analyze_logcat: inconsistent measurements: '
                                        'start: %s, '
                                        'throbber start: %s, throbber stop: %s ' %
                                      (start_time,
                                       throbber_start_time,
                                       throbber_stop_time))
                start_time = throbber_start_time = throbber_stop_time = 0
        else:
            self.loggerdeco.warning(
                'analyze_logcat: failed to get measurements '
                'start_time: %s, throbber start: %s, throbber stop: %s' % (
                    start_time, throbber_start_time, throbber_stop_time))
            start_time = throbber_start_time = throbber_stop_time = 0

        return (start_time, throbber_start_time, throbber_stop_time)
Example #16
0
class UnitTest(PhoneTest):
    def runjob(self, build_metadata, worker_subprocess):
        logger = self.logger
        loggerdeco = self.loggerdeco
        self.logger = logging.getLogger('autophone.worker.subprocess.test')
        self.loggerdeco = LogDecorator(
            self.logger, {
                'phoneid': self.phone_cfg['phoneid'],
                'phoneip': self.phone_cfg['ip'],
                'buildid': build_metadata['buildid']
            }, '%(phoneid)s|%(phoneip)s|%(buildid)s|'
            '%(message)s')
        try:
            self.setup_and_runtest(build_metadata, worker_subprocess)
        finally:
            self.logger = logger
            self.loggerdeco = loggerdeco

    def setup_and_runtest(self, build_metadata, worker_subprocess):
        self.loggerdeco.debug('runtestsremote.py runjob start')
        self.set_status(msg='runtestsremote.py runjob start')

        self.worker_subprocess = worker_subprocess
        self.worker_subprocess.check_sdcard()

        host_ip_address = self.phone_cfg['ipaddr']
        phone_ip_address = self.phone_cfg['ip']
        device_port = self.phone_cfg['sutcmdport']

        cache_build_dir = os.path.abspath(build_metadata["cache_build_dir"])
        symbols_path = os.path.join(cache_build_dir, 'symbols')
        if not os.path.exists(symbols_path):
            symbols_path = None

        androidprocname = build_metadata['androidprocname']
        re_revision = re.compile(r'http.*/rev/(.*)')
        match = re_revision.match(build_metadata['revision'])
        if match:
            revision = match.group(1)
        else:
            revision = build_metadata['revision']
        buildid = build_metadata['buildid']
        tree = build_metadata['tree']

        if self.logger.getEffectiveLevel() == logging.DEBUG:
            for prop in self.phone_cfg:
                self.loggerdeco.debug('phone_cfg[%s] = %s' %
                                      (prop, self.phone_cfg[prop]))

        job_cfg = ConfigParser.RawConfigParser()
        job_cfg.read(self.config_file)

        if job_cfg.has_option('runtests', 'config_files'):
            # job contains multiple unittests
            config_files = job_cfg.get('runtests', 'config_files').split(' ')
        elif job_cfg.has_option('runtests', 'test_name'):
            # job contains a single unittest
            config_files = [self.config_file]
        else:
            raise Exception('Job configuration %s does not specify a test' %
                            self.config_file)
        missing_config_files = []
        for config_file in config_files:
            if not os.path.exists(config_file):
                missing_config_files.append(config_file)
        if missing_config_files:
            raise Exception("Can not run tests with missing config files: %s" %
                            ', '.join(missing_config_files))
        for config_file in config_files:
            try:
                test_parameters = {
                    'host_ip_address': host_ip_address,
                    'phone_ip_address': phone_ip_address,
                    'device_port': device_port,
                    'androidprocname': androidprocname,
                    'cache_build_dir': cache_build_dir,
                    'symbols_path': symbols_path,
                    'revision': revision,
                    'buildid': buildid,
                    'tree': tree,
                    'config_file': config_file,
                }

                self.load_test_parameters(test_parameters, config_file)
                self.runtest(test_parameters)

            except:
                # This exception handler deals with exceptions which occur outside
                # of the actual test runner. Exceptions from the test runner
                # are handled locally in runtest.
                self.loggerdeco.exception(
                    'runtestsremote.py:runjob: Exception '
                    'running test')
                self.set_status(msg='runtestsremote.py:runjob: Exception '
                                'running test')
                # give the phone a minute to recover
                time.sleep(60)
                self.worker_subprocess.recover_phone()

        self.loggerdeco.debug('runtestsremote.py runjob exit')
        self.set_status(msg='runtestsremote.py runjob exit')

    def load_test_parameters(self, test_parameters, config_file):

        cfg = ConfigParser.RawConfigParser()
        cfg.read(config_file)

        test_parameters['test_name'] = cfg.get('runtests', 'test_name')
        test_parameters['test_manifest'] = cfg.get('runtests', 'test_manifest')
        try:
            test_parameters['test_path'] = cfg.get('runtests', 'test_path')
        except ConfigParser.NoOptionError:
            test_parameters['test_path'] = None

        unittest_config_file = cfg.get('runtests', 'unittest_defaults')
        cfg.read(unittest_config_file)

        self.loggerdeco.info('config_file = %s, unittest_config_file = %s' %
                             (config_file, unittest_config_file))

        test_parameters['xre_path'] = cfg.get('runtests', 'xre_path')
        test_parameters['utility_path'] = cfg.get('runtests', 'utility_path')
        if test_parameters['symbols_path'] is not None and cfg.has_option(
                'runtests', 'minidump_stackwalk'):
            test_parameters['minidump_stackwalk'] = cfg.get(
                'runtests', 'minidump_stackwalk')
            os.environ['MINIDUMP_STACKWALK'] = test_parameters[
                'minidump_stackwalk']
        elif 'MINIDUMP_STACKWALK' in os.environ:
            del os.environ['MINIDUMP_STACKWALK']

        if cfg.has_option('runtests', 'androidprocname'):
            test_parameters['androidprocname'] = cfg.get(
                'runtests', 'androidprocname')

        test_parameters['console_level'] = cfg.get('runtests', 'console_level')
        test_parameters['file_level'] = cfg.get('runtests', 'file_level')
        test_parameters['time_out'] = cfg.getint('runtests', 'time_out')

        if cfg.has_option('runtests', 'iterations'):
            test_parameters['iterations'] = cfg.getint('runtests',
                                                       'iterations')
        else:
            test_parameters['iterations'] = 1

        if cfg.has_option('runtests', 'total_chunks'):
            test_parameters['total_chunks'] = cfg.getint(
                'runtests', 'total_chunks')
        else:
            test_parameters['total_chunks'] = 1

        if cfg.has_option('runtests', 'prefs'):
            test_parameters['prefs'] = cfg.get('runtests', 'prefs').split(',')
        else:
            test_parameters['prefs'] = []

        test_parameters['es_server'] = cfg.get('autolog', 'es_server')
        test_parameters['rest_server'] = cfg.get('autolog', 'rest_server')
        test_parameters['index'] = cfg.get('autolog', 'index')
        test_parameters['include_pass'] = cfg.getboolean(
            'autolog', 'include_pass')
        test_parameters['submit_log'] = cfg.getboolean('autolog', 'submit_log')
        test_parameters['use_newparser'] = cfg.getboolean(
            'autolog', 'use_newparser')

    def create_test_args(self, test_parameters):
        args = ['python']

        test_name_lower = test_parameters['test_name'].lower()

        if test_name_lower.startswith('robocoptest'):
            test_args = [
                'mochitest/runtestsremote.py',
                '--robocop=%s' % test_parameters['test_manifest'],
                '--robocop-ids=%s/fennec_ids.txt' %
                test_parameters['cache_build_dir'],
                '--certificate-path=certs',
                '--close-when-done',
                '--autorun',
                '--console-level=%s' % test_parameters['console_level'],
                '--file-level=%s' % test_parameters['file_level'],
                '--repeat=%d' % test_parameters['iterations'],
            ]
        elif test_name_lower.startswith('mochitest'):
            test_args = [
                'mochitest/runtestsremote.py',
                '--test-manifest=%s' % test_parameters['test_manifest'],
                '--test-path=%s' % test_parameters['test_path'],
                '--certificate-path=certs',
                '--close-when-done',
                '--autorun',
                '--console-level=%s' % test_parameters['console_level'],
                '--file-level=%s' % test_parameters['file_level'],
                '--repeat=%d' % test_parameters['iterations'],
            ]
        elif test_name_lower.startswith('reftest'):
            test_args = [
                'reftest/remotereftest.py',
                '--enable-privilege',
                '--ignore-window-size',
                '--bootstrap',
                '%s' % test_parameters['test_manifest'],
            ]
        elif test_name_lower.startswith('jsreftest'):
            test_args = [
                'reftest/remotereftest.py',
                '--enable-privilege',
                '--ignore-window-size',
                '--bootstrap',
                '--extra-profile-file=jsreftest/tests/user.js',
                '%s' % test_parameters['test_manifest'],
            ]
        elif test_name_lower.startswith('crashtest'):
            test_args = [
                'reftest/remotereftest.py',
                '--enable-privilege',
                '--ignore-window-size',
                '--bootstrap',
                '%s' % test_parameters['test_manifest'],
            ]
        else:
            self.loggerdeco.error('Unknown test_name %s' %
                                  test_parameters['test_name'])
            raise Exception('Unknown test_name %s' %
                            test_parameters['test_name'])

        test_parameters['http_port'] = test_parameters['port_manager'].reserve(
        )
        test_parameters['ssl_port'] = test_parameters['port_manager'].reserve()

        common_args = [
            '--deviceIP=%s' % test_parameters['phone_ip_address'],
            '--devicePort=%s' % test_parameters['device_port'],
            '--app=%s' % test_parameters['androidprocname'],
            '--xre-path=%s' % test_parameters['xre_path'],
            '--utility-path=%s' % test_parameters['utility_path'],
            '--timeout=%d' % test_parameters['time_out'],
            '--http-port=%s' %
            test_parameters['port_manager'].use(test_parameters['http_port']),
            '--ssl-port=%s' %
            test_parameters['port_manager'].use(test_parameters['ssl_port']),
            '--total-chunks=%d' % test_parameters['total_chunks'],
            '--this-chunk=%d' % test_parameters['this_chunk'],
            '--log-file=%s-%s.log' % (test_parameters['test_name'],
                                      test_parameters['phone_ip_address']),
            '--pidfile=%s-%s.pid' % (test_parameters['test_name'],
                                     test_parameters['phone_ip_address']),
        ]
        for pref in test_parameters['prefs']:
            common_args.append('--setpref=%s' % pref)

        args.extend(test_args)
        args.extend(common_args)

        if test_parameters['symbols_path'] is not None:
            args.append('--symbols-path=%s' % test_parameters['symbols_path'])

        return args

    def process_test_log(self, test_parameters, logfilehandle):

        test_log = None
        test_runs = []

        if test_parameters['use_newparser']:
            logfilehandle.close()
            logfilehandle = open(logfilehandle.name)
            try:
                # Turn off verbose logging for the log parser
                logger = logging.getLogger('logparser')
                logger_effectiveLevel = logger.getEffectiveLevel()
                logger.setLevel(logging.WARN)
                test_log = newlogparser.parse_log(logfilehandle)
                test_runs = test_log.convert(test_parameters['include_pass'])
            finally:
                logger.setLevel(logger_effectiveLevel)
                logfilehandle.close()
        else:
            lp = LogParser([logfilehandle.name],
                           es=False,
                           es_server=None,
                           includePass=True,
                           output_dir=None,
                           logger=self.logger,
                           harnessType=test_parameters['harness_type'])

            # Use logparser's parsers, but do not allow it to
            # submit data directly to elasticsearch.
            test_runs.append(lp.parseFiles())

        if test_parameters['es_server'] is None or test_parameters[
                'rest_server'] is None:
            return

        # testgroup must match entry in autolog/js/Config.js:testNames
        # os        must match entry in autolog/js/Config.js:OSNames
        # platform  must match entry in autolog/js/Config.js:OSNames

        logfilename = None
        if test_parameters['submit_log']:
            logfilename = logfilehandle.name

        chunk_descriptor = ''
        if test_parameters['total_chunks'] > 1:
            chunk_descriptor = 's-%d' % test_parameters['this_chunk']

        testgroup_name = '%s%s' % (test_parameters['test_name'],
                                   chunk_descriptor)

        platform_name = self.phone_cfg['machinetype']

        self.loggerdeco.debug('testgroup_name = %s' % testgroup_name)

        testgroup = RESTfulAutologTestGroup(
            index=test_parameters['index'],
            testgroup=testgroup_name,
            os='android',
            platform=platform_name,
            harness=test_parameters['harness_type'],
            server=test_parameters['es_server'],
            restserver=test_parameters['rest_server'],
            machine=self.phone_cfg['phoneid'],
            logfile=logfilename)

        testgroup.set_primary_product(tree=test_parameters['tree'],
                                      buildtype='opt',
                                      buildid=test_parameters['buildid'],
                                      revision=test_parameters['revision'])

        for testdata in test_runs:

            if self.logger.getEffectiveLevel() == logging.DEBUG:
                self.loggerdeco.debug('Begin testdata')
                self.loggerdeco.debug(json.dumps(testdata, indent=4))
                self.loggerdeco.debug('End testdata')

            testgroup.add_test_suite(testsuite=testgroup_name,
                                     cmdline=test_parameters['cmdline'],
                                     passed=testdata.get('passed', None),
                                     failed=testdata.get('failed', None),
                                     todo=testdata.get('todo', None))

            for t in testdata.get('failures', {}):
                test = t["test"]
                for f in t["failures"]:
                    text = f["text"]
                    status = f["status"]
                    testgroup.add_test_failure(test=test,
                                               text=text,
                                               status=status)

            # Submitting passing tests not supported via REST API
            if test_parameters['include_pass']:
                for t in testdata.get('passes', {}):
                    test = t["test"]
                    duration = None
                    if "duration" in t:
                        duration = t["duration"]
                    testgroup.add_test_pass(test=test, duration=duration)

        testgroup.submit()

    def runtest(self, test_parameters):

        self.loggerdeco = LogDecorator(
            self.logger, {
                'phoneid': self.phone_cfg['phoneid'],
                'phoneip': self.phone_cfg['ip'],
                'buildid': test_parameters['buildid'],
                'testname': test_parameters['test_name']
            }, '%(phoneid)s|%(phoneip)s|%(buildid)s|'
            '%(testname)s|%(message)s')

        if self.logger.getEffectiveLevel() == logging.DEBUG:
            self.loggerdeco.debug('runtestsremote.py runtest start')
            for key in test_parameters.keys():
                self.loggerdeco.debug('test parameters: %s = %s' %
                                      (key, test_parameters[key]))

        self.set_status(msg='Starting test %s' % test_parameters['test_name'])

        test_parameters['harness_type'] = test_parameters['test_name']

        if test_parameters['test_name'] == 'robocoptest':
            test_parameters['harness_type'] = 'mochitest'
            robocop_apk_path = posixpath.join(self.dm.getDeviceRoot(),
                                              'robocop.apk')

            # XXX: FIXME. When bug 792072 lands, change to have
            # installApp() push the file

            self.dm.pushFile(
                os.path.join(test_parameters['cache_build_dir'],
                             'robocop.apk'), robocop_apk_path)
            try:
                self.dm.uninstallApp('org.mozilla.roboexample.test')
            except DMError:
                self.loggerdeco.exception(
                    'runtestsremote.py:runtest: Exception running test.')

            self.dm.installApp(robocop_apk_path)
            self.dm.removeFile(robocop_apk_path)

        test_parameters['port_manager'] = PortManager(
            test_parameters['host_ip_address'])

        for this_chunk in range(1, test_parameters['total_chunks'] + 1):

            test_parameters['this_chunk'] = this_chunk

            try:
                while True:
                    socket_collision = False

                    logfilehandle = tempfile.NamedTemporaryFile(delete=False)
                    self.loggerdeco.debug('logging to %s' % logfilehandle.name)

                    args = self.create_test_args(test_parameters)

                    test_parameters['cmdline'] = ' '.join(args)
                    self.loggerdeco.debug("cmdline = %s" %
                                          test_parameters['cmdline'])

                    self.set_status(msg='Running test %s chunk %d of %d' %
                                    (test_parameters['test_name'], this_chunk,
                                     test_parameters['total_chunks']))
                    if self.dm.processExist(
                            test_parameters['androidprocname']):
                        max_kill_attempts = 3
                        for kill_attempt in range(max_kill_attempts):
                            self.loggerdeco.debug(
                                'Process %s exists. Attempt %d to kill.' %
                                (test_parameters['androidprocname'],
                                 kill_attempt + 1))
                            self.dm.killProcess(
                                test_parameters['androidprocname'])
                            if not self.dm.processExist(
                                    test_parameters['androidprocname']):
                                break
                        if kill_attempt == max_kill_attempts - 1 and \
                                self.dm.processExist(test_parameters['androidprocname']):
                            self.loggerdeco.warning(
                                'Could not kill process %s.' %
                                (test_parameters['androidprocname']))
                    proc = subprocess.Popen(
                        args,
                        cwd=os.path.join(test_parameters['cache_build_dir'],
                                         'tests'),
                        preexec_fn=lambda: os.setpgid(0, 0),
                        stdout=logfilehandle,
                        stderr=subprocess.STDOUT,
                        close_fds=True)
                    proc.wait()
                    self.loggerdeco.debug('runtestsremote.py return code %d' %
                                          proc.returncode)

                    logfilehandle.close()
                    reSocketError = re.compile('socket\.error:')
                    logfilehandle = open(logfilehandle.name)
                    for logline in logfilehandle:
                        if reSocketError.search(logline):
                            socket_collision = True
                            break
                    logfilehandle.close()

                    if not socket_collision:
                        break

                self.set_status(msg='Completed test %s chunk %d of %d' %
                                (test_parameters['test_name'], this_chunk,
                                 test_parameters['total_chunks']))
            except:
                error_message = (
                    'Exception during test %s chunk %d of %d: %s' %
                    (test_parameters['test_name'], this_chunk,
                     test_parameters['total_chunks'], traceback.format_exc()))
                self.set_status(msg=error_message)
                self.loggerdeco.error(error_message)
            finally:
                logfilehandle.close()
                self.process_test_log(test_parameters, logfilehandle)
                if self.logger.getEffectiveLevel() == logging.DEBUG:
                    logfilehandle = open(logfilehandle.name)
                    self.loggerdeco.debug(40 * '*')
                    self.loggerdeco.debug(logfilehandle.read())
                    self.loggerdeco.debug(40 * '-')
                    logfilehandle.close()
                os.unlink(logfilehandle.name)
                # wait for a minute to give the phone time to settle
                time.sleep(60)
                # Recover the phone in between tests/chunks.
                self.loggerdeco.info('Rebooting device after test.')
                self.worker_subprocess.recover_phone()

        self.loggerdeco.debug('runtestsremote.py runtest exit')

        return
Example #17
0
class S1S2Test(PhoneTest):

    def runjob(self, build_metadata, worker_subprocess):
        logger = self.logger
        loggerdeco = self.loggerdeco
        self.logger = logging.getLogger('autophone.worker.subprocess.test')
        self.loggerdeco = LogDecorator(self.logger,
                                       {'phoneid': self.phone_cfg['phoneid'],
                                        'phoneip': self.phone_cfg['ip'],
                                        'buildid': build_metadata['buildid']},
                                       '%(phoneid)s|%(phoneip)s|%(buildid)s|'
                                       '%(message)s')
        self.dm._logger = self.loggerdeco

        try:
            # Read our config file which gives us our number of
            # iterations and urls that we will be testing
            cfg = ConfigParser.RawConfigParser()
            cfg.read(self.config_file)
            # [signature]
            self._signer = None
            self._jwt = {'id': '', 'key': None}
            for opt in self._jwt.keys():
                try:
                    self._jwt[opt] = cfg.get('signature', opt)
                except (ConfigParser.NoSectionError,
                        ConfigParser.NoOptionError):
                    break
            # phonedash requires both an id and a key.
            if self._jwt['id'] and self._jwt['key']:
                self._signer = jwt.jws.HmacSha(key=self._jwt['key'],
                                               key_id=self._jwt['id'])
            # [paths]
            autophone_directory = os.path.dirname(os.path.abspath(sys.argv[0]))
            self._paths = {}
            self._paths['source'] = os.path.join(autophone_directory, 'files/')
            self._paths['dest'] = posixpath.join(self.base_device_path, 's1test/')
            self._paths['remote'] = 'http://phonedash.mozilla.org/'
            try:
                for opt in ('source', 'dest', 'profile', 'remote'):
                    try:
                        self._paths[opt] = cfg.get('paths', opt)
                        if not self._paths[opt].endswith('/'):
                            self._paths[opt] += '/'
                    except ConfigParser.NoOptionError:
                        pass
            except ConfigParser.NoSectionError:
                pass
            if 'profile' in self._paths:
                self.profile_path = self._paths['profile']
            # _pushes = {'sourcepath' : 'destpath', ...}
            self._pushes = {}
            for push in glob.glob(self._paths['source'] + '*'):
                if push.endswith('~') or push.endswith('.bak'):
                    continue
                push_dest = posixpath.join(self._paths['dest'], os.path.basename(push))
                self._pushes[push] = push_dest
            # [tests]
            self._tests = {}
            for t in cfg.items('tests'):
                self._tests[t[0]] = t[1]
            # Map URLS - {urlname: url} - urlname serves as testname
            self._urls = {}
            for test_location in ('local', 'remote'):
                for test_name in self._tests:
                    if test_location == 'local':
                        test_url = 'file://' + self._paths['dest'] + self._tests[test_name]
                    else:
                        test_url = self._paths['remote'] + self._tests[test_name]
                    self._urls["%s-%s" % (test_location, test_name)] = test_url
            # [settings]
            self._iterations = cfg.getint('settings', 'iterations')
            try:
                self.stderrp_accept = cfg.getfloat('settings', 'stderrp_accept')
            except ConfigParser.NoOptionError:
                self.stderrp_accept = 0
            try:
                self.stderrp_reject = cfg.getfloat('settings', 'stderrp_reject')
            except ConfigParser.NoOptionError:
                self.stderrp_reject = 100
            try:
                self.stderrp_attempts = cfg.getint('settings', 'stderrp_attempts')
            except ConfigParser.NoOptionError:
                self.stderrp_attempts = 1
            self._resulturl = cfg.get('settings', 'resulturl')
            if not self._resulturl.endswith('/'):
                self._resulturl += '/'
            self._initialize_url = 'file://' + self._paths['dest'] + 'initialize_profile.html'

            self.runtests(build_metadata, worker_subprocess)
        finally:
            self.logger = logger
            self.loggerdeco = loggerdeco
            self.dm._logger = loggerdeco

    def is_stderr_below_threshold(self, dataset, threshold):
        """Return True if all of the measurements in the dataset have
        standard errors of the mean below the threshold.

        Return False if at least one measurement is above the threshold
        or if one or more datasets have only one value.

        Return None if at least one measurement has no values.
        """

        self.loggerdeco.debug("is_stderr_below_threshold: %s" % dataset)

        for cachekey in ('uncached', 'cached'):
            for measurement in ('throbberstart', 'throbberstop'):
                data = [datapoint[cachekey][measurement] - datapoint[cachekey]['starttime']
                        for datapoint in dataset
                        if datapoint and cachekey in datapoint]
                if not data:
                    return None
                stats = get_stats(data)
                self.loggerdeco.debug('%s %s count: %d, mean: %.2f, '
                                      'stddev: %.2f, stderr: %.2f, '
                                      'stderrp: %.2f' % (
                                          cachekey, measurement,
                                          stats['count'], stats['mean'],
                                          stats['stddev'], stats['stderr'],
                                          stats['stderrp']))
                if stats['count'] == 1 or stats['stderrp'] >= threshold:
                    return False
        return True

    def runtests(self, build_metadata, worker_subprocess):
        self.loggerdeco = LogDecorator(self.logger,
                                       {'phoneid': self.phone_cfg['phoneid'],
                                        'phoneip': self.phone_cfg['ip'],
                                        'buildid': build_metadata['buildid']},
                                       '%(phoneid)s|%(phoneip)s|%(buildid)s|'
                                       '%(message)s')
        self.dm._logger = self.loggerdeco
        appname = build_metadata['androidprocname']

        if not self.install_local_pages():
            self.set_status(msg='Could not install local pages on phone. '
                            'Aborting test for '
                            'build %s' % build_metadata['buildid'])
            return

        if not self.create_profile(build_metadata):
            self.set_status(msg='Could not run Fennec. Aborting test for '
                            'build %s' % build_metadata['buildid'])
            return

        testcount = len(self._urls.keys())
        for testnum,(testname,url) in enumerate(self._urls.iteritems(), 1):
            self.loggerdeco = LogDecorator(self.logger,
                                           {'phoneid': self.phone_cfg['phoneid'],
                                            'phoneip': self.phone_cfg['ip'],
                                            'buildid': build_metadata['buildid'],
                                            'testname': testname},
                                           '%(phoneid)s|%(phoneip)s|%(buildid)s|'
                                           '%(testname)s|%(message)s')
            self.dm._logger = self.loggerdeco
            if self.check_results(build_metadata, testname):
                # We already have good results for this test and build.
                # No need to test it again.
                self.loggerdeco.info('Skipping test (%d/%d) for %d iterations' %
                                     (testnum, testcount, self._iterations))
                continue
            self.loggerdeco.info('Running test (%d/%d) for %d iterations' %
                                 (testnum, testcount, self._iterations))

            for attempt in range(self.stderrp_attempts):
                # dataset is a list of the measurements made for the
                # iterations for this test.
                #
                # An empty item in the dataset list represents a
                # failure to obtain any measurement for that
                # iteration.
                #
                # It is possible for an item in the dataset to have an
                # uncached value and not have a corresponding cached
                # value if the cached test failed to record the
                # values.

                dataset = []
                for iteration in range(self._iterations):
                    self.set_status(msg='Attempt %d/%d for Test %d/%d, '
                                    'run %d, for url %s' %
                                    (attempt+1, self.stderrp_attempts,
                                     testnum, testcount, iteration+1, url))

                    dataset.append({})

                    if not self.create_profile(build_metadata):
                        continue

                    measurement = self.runtest(build_metadata, appname, url)
                    if not measurement:
                        continue
                    dataset[-1]['uncached'] = measurement

                    measurement = self.runtest(build_metadata, appname, url)
                    if not measurement:
                        continue
                    dataset[-1]['cached'] = measurement

                    if self.is_stderr_below_threshold(dataset,
                                                      self.stderrp_accept):
                        self.loggerdeco.info(
                            'Accepted test (%d/%d) after %d of %d iterations' %
                            (testnum, testcount, iteration+1, self._iterations))
                        break

                self.loggerdeco.debug('publishing results')

                if self.is_stderr_below_threshold(dataset, self.stderrp_reject):
                    rejected = False
                else:
                    rejected = True
                    self.loggerdeco.info(
                        'Rejected test (%d/%d) after %d/%d iterations' %
                        (testnum, testcount, iteration+1, self._iterations))

                for datapoint in dataset:
                    for cachekey in datapoint:
                        self.publish_results(
                            starttime=datapoint[cachekey]['starttime'],
                            tstrt=datapoint[cachekey]['throbberstart'],
                            tstop=datapoint[cachekey]['throbberstop'],
                            build_metadata=build_metadata,
                            testname=testname,
                            cache_enabled=(cachekey=='cached'),
                            rejected=rejected)
                if not rejected:
                    break

    def runtest(self, build_metadata, appname, url):
        # Clear logcat
        self.loggerdeco.debug('clearing logcat')
        self.dm.recordLogcat()
        self.loggerdeco.debug('logcat cleared')
        self.loggerdeco.debug('running fennec')

        # Run test
        self.run_fennec_with_profile(appname, url)

        # Get results - do this now so we don't have as much to
        # parse in logcat.
        starttime, throbberstart, throbberstop = self.analyze_logcat(
            build_metadata)

        self.wait_for_fennec(build_metadata)

        # Ensure we succeeded - no 0's reported
        datapoint = {}
        if (throbberstart and throbberstop):
            datapoint['starttime'] = starttime
            datapoint['throbberstart'] = throbberstart
            datapoint['throbberstop'] = throbberstop
            datapoint['throbbertime'] = throbberstop - throbberstart
        return datapoint

    def wait_for_fennec(self, build_metadata, max_wait_time=60, wait_time=5,
                        kill_wait_time=20):
        # Wait for up to a max_wait_time seconds for fennec to close
        # itself in response to the quitter request. Check that fennec
        # is still running every wait_time seconds. If fennec doesn't
        # close on its own, attempt up to 3 times to kill fennec, waiting
        # kill_wait_time seconds between attempts.
        # Return True if fennec exits on its own, False if it needs to be killed.
        # Re-raise the last exception if fennec can not be killed.
        max_wait_attempts = max_wait_time / wait_time
        for wait_attempt in range(max_wait_attempts):
            if not self.dm.processExist(build_metadata['androidprocname']):
                return True
            sleep(wait_time)
        self.loggerdeco.debug('killing fennec')
        max_killattempts = 3
        for kill_attempt in range(max_killattempts):
            try:
                self.dm.killProcess(build_metadata['androidprocname'])
                break
            except DMError:
                self.loggerdeco.exception('Attempt %d to kill fennec failed' %
                                          kill_attempt)
                if kill_attempt == max_killattempts - 1:
                    raise
                sleep(kill_wait_time)
        return False

    def create_profile(self, build_metadata, custom_prefs=None):
        # Create, install and initialize the profile to be
        # used in the test.

        telemetry_prompt = 999
        if build_metadata['blddate'] < '2013-01-03':
            telemetry_prompt = 2
        prefs = {
            'browser.firstrun.show.localepicker': False,
            'browser.sessionstore.resume_from_crash': False,
            'dom.ipc.plugins.flash.subprocess.crashreporter.enabled': False,
            'browser.firstrun.show.uidiscovery': False,
            'shell.checkDefaultClient': False,
            'browser.warnOnQuit': False,
            'browser.EULA.override': True,
            'toolkit.telemetry.prompted': telemetry_prompt,
            'toolkit.telemetry.notifiedOptOut': telemetry_prompt,
            'datareporting.healthreport.service.enabled': False,
            }
        if isinstance(custom_prefs, dict):
            prefs = dict(prefs.items() + custom_prefs.items())
        profile = FirefoxProfile(preferences=prefs, addons='%s/xpi/quitter.xpi' %
                                 os.getcwd())
        if not self.install_profile(profile):
            return False

        appname = build_metadata['androidprocname']
        buildid = build_metadata['buildid']
        success = False
        for attempt in range(self.user_cfg[PHONE_RETRY_LIMIT]):
            self.loggerdeco.debug('Attempt %d Initializing profile' % attempt)
            self.run_fennec_with_profile(appname, self._initialize_url)
            if self.wait_for_fennec(build_metadata):
                success = True
                break
            sleep(self.user_cfg[PHONE_RETRY_WAIT])

        if not success:
            msg = 'Failure initializing profile for build %s' % buildid
            self.loggerdeco.error(msg)
            self.set_status(msg=msg)

        return success

    def install_local_pages(self):
        success = False
        for attempt in range(self.user_cfg[PHONE_RETRY_LIMIT]):
            self.loggerdeco.debug('Attempt %d Installing local pages' % attempt)
            try:
                self.dm.mkDir(self._paths['dest'])
                for push_source in self._pushes:
                    push_dest = self._pushes[push_source]
                    if os.path.isdir(push_source):
                        self.dm.pushDir(push_source, push_dest)
                    else:
                        self.dm.pushFile(push_source, push_dest)
                success = True
                break
            except DMError:
                self.loggerdeco.exception('Attempt %d Installing local pages' % attempt)
                sleep(self.user_cfg[PHONE_RETRY_WAIT])

        if not success:
            self.loggerdeco.error('Failure installing local pages')

        return success

    def is_fennec_running(self, appname):
        for attempt in range(self.user_cfg[PHONE_RETRY_LIMIT]):
            try:
                return self.dm.processExist(appname)
            except DMError:
                self.loggerdeco.exception('Attempt %d is fennec running' % attempt)
                if attempt == self.user_cfg[PHONE_RETRY_LIMIT] - 1:
                    raise
                sleep(self.user_cfg[PHONE_RETRY_WAIT])

    def get_logcat_throbbers(self):
        for attempt in range(self.user_cfg[PHONE_RETRY_LIMIT]):
            try:
                return [x.strip() for x in
                        self.dm.getLogcat(
                            filterSpecs=['GeckoToolbarDisplayLayout:*', 'SUTAgentAndroid:I', '*:S'])]
            except DMError:
                self.loggerdeco.exception('Attempt %d get logcat throbbers' % attempt)
                if attempt == self.user_cfg[PHONE_RETRY_LIMIT] - 1:
                    raise
                sleep(self.user_cfg[PHONE_RETRY_WAIT])

    def analyze_logcat(self, build_metadata):
        self.loggerdeco.debug('analyzing logcat')

        app_name = build_metadata['androidprocname']
        logcat_prefix = '(\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3})'
        throbber_prefix = 'I/GeckoToolbarDisplayLayout.* zerdatime (\d+) - Throbber'
        re_base_time = re.compile('%s' % logcat_prefix)
        re_start_time = re.compile('%s I/SUTAgentAndroid.* '
                                   'exec am start\s+.*-n %s/.App '
                                   '-a android.intent.action.VIEW' %
                                   (logcat_prefix, app_name))
        re_throbber_start_time = re.compile('%s %s start' %
                                            (logcat_prefix, throbber_prefix))
        re_throbber_stop_time = re.compile('%s %s stop' %
                                           (logcat_prefix, throbber_prefix))

        self.loggerdeco.debug('analyze_logcat: re_base_time: %s' % re_base_time.pattern)
        self.loggerdeco.debug('analyze_logcat: re_start_time: %s' % re_start_time.pattern)
        self.loggerdeco.debug('analyze_logcat: re_throbber_start_time: %s' % re_throbber_start_time.pattern)
        self.loggerdeco.debug('analyze_logcat: re_throbber_stop_time: %s' % re_throbber_stop_time.pattern)

        base_time = 0
        start_time = 0
        throbber_start_time = 0
        throbber_stop_time = 0

        attempt = 0
        max_time = 90 # maximum time to wait for throbbers
        wait_time = 3 # time to wait between attempts
        max_attempts = max_time / wait_time

        while (attempt < max_attempts and (throbber_start_time == 0 or
                                           throbber_stop_time == 0)):
            buf = self.get_logcat_throbbers()
            for line in buf:
                self.loggerdeco.debug('analyze_logcat: %s' % line)
                match = re_base_time.match(line)
                if match and not base_time:
                    base_time = match.group(1)
                    self.loggerdeco.debug('analyze_logcat: base_time: %s' % base_time)
                # we want the first throbberstart and throbberstop.
                match = re_start_time.match(line)
                if match:
                    start_time = match.group(1)
                    self.loggerdeco.debug('analyze_logcat: start_time: %s' % start_time)
                    continue
                match = re_throbber_start_time.match(line)
                if match and not throbber_start_time:
                    throbber_start_time = match.group(1)
                    self.loggerdeco.debug('analyze_logcat: throbber_start_time: %s' % throbber_start_time)
                    continue
                match = re_throbber_stop_time.match(line)
                if match and not throbber_stop_time:
                    throbber_stop_time = match.group(1)
                    self.loggerdeco.debug('analyze_logcat: throbber_stop_time: %s' % throbber_stop_time)
                    continue
                if throbber_start_time and throbber_stop_time:
                    break
            if throbber_start_time == 0 or throbber_stop_time == 0:
                sleep(wait_time)
                attempt += 1
        if self.check_for_crashes():
            self.loggerdeco.info('fennec crashed')
            fennec_crashed = True
        else:
            fennec_crashed = False
        if throbber_start_time and throbber_stop_time == 0 and not fennec_crashed:
            self.loggerdeco.info('Unable to find Throbber stop')

        # The captured time from the logcat lines is in the format
        # MM-DD HH:MM:SS.mmm. It is possible for the year to change
        # between the different times, so we need to make adjustments
        # if necessary. First, we assume the year does not change and
        # parse the dates as if they are in the current year. If
        # the dates violate the natural order start_time,
        # throbber_start_time, throbber_stop_time, we can adjust the
        # year.

        if base_time and start_time and throbber_start_time and throbber_stop_time:
            parse = lambda y, t: datetime.datetime.strptime('%4d-%s' % (y, t), '%Y-%m-%d %H:%M:%S.%f')
            year = datetime.datetime.now().year
            base_time = parse(year, base_time)
            start_time = parse(year, start_time)
            throbber_start_time = parse(year, throbber_start_time)
            throbber_stop_time = parse(year, throbber_stop_time)

            if base_time > start_time:
                base_time.replace(year=year-1)
            elif start_time > throbber_start_time:
                base_time.replace(year=year-1)
                start_time.replace(year=year-1)
            elif throbber_start_time > throbber_stop_time:
                base_time.replace(year=year-1)
                start_time.replace(year=year-1)
                throbber_start_time.replace(year-1)

            # Convert the times to milliseconds from the base time.
            convert = lambda t1, t2: round((t2 - t1).total_seconds() * 1000.0)

            start_time = convert(base_time, start_time)
            throbber_start_time = convert(base_time, throbber_start_time)
            throbber_stop_time = convert(base_time, throbber_stop_time)

            self.loggerdeco.debug('analyze_logcat: base: %s, start: %s, '
                                  'throbber start: %s, throbber stop: %s, '
                                  'throbber time: %s ' %
                                  (base_time, start_time,
                                   throbber_start_time, throbber_stop_time,
                                   throbber_stop_time - throbber_start_time))
        else:
            self.loggerdeco.warning(
                'analyze_logcat: failed to get throbbers '
                'start_time: %s, throbber start: %s, throbber stop: %s' % (
                    start_time, throbber_start_time, throbber_stop_time))
            start_time = throbber_start_time = throbber_stop_time = 0

        return (start_time, throbber_start_time, throbber_stop_time)

    def publish_results(self, starttime=0, tstrt=0, tstop=0,
                        build_metadata=None, testname='', cache_enabled=True,
                        rejected=False):
        msg = ('Cached: %s Start Time: %s Throbber Start: %s Throbber Stop: %s '
               'Total Throbber Time: %s Rejected: %s' % (
                   cache_enabled, starttime, tstrt, tstop, tstop - tstrt, rejected))
        self.loggerdeco.info('RESULTS: %s' % msg)

        # Create JSON to send to webserver
        resultdata = {}
        resultdata['phoneid'] = self.phone_cfg['phoneid']
        resultdata['testname'] = testname
        resultdata['starttime'] = starttime
        resultdata['throbberstart'] = tstrt
        resultdata['throbberstop'] = tstop
        resultdata['blddate'] = build_metadata['blddate']
        resultdata['cached'] = cache_enabled
        resultdata['rejected'] = rejected

        resultdata['revision'] = build_metadata['revision']
        resultdata['productname'] = build_metadata['androidprocname']
        resultdata['productversion'] = build_metadata['version']
        resultdata['osver'] = self.phone_cfg['osver']
        resultdata['bldtype'] = build_metadata['bldtype']
        resultdata['machineid'] = self.phone_cfg['machinetype']

        result = {'data': resultdata}
        # Upload
        if self._signer:
            encoded_result = jwt.encode(result, signer=self._signer)
            content_type = 'application/jwt'
        else:
            encoded_result = json.dumps(result)
            content_type = 'application/json; charset=utf-8'
        req = urllib2.Request(self._resulturl + 'add/', encoded_result,
                              {'Content-Type': content_type})
        try:
            f = urllib2.urlopen(req)
        except urllib2.URLError, e:
            self.loggerdeco.error('Could not send results to server: %s' % e)
        else:
Example #18
0
class PhoneWorker(object):
    """Runs tests on a single phone in a separate process.
    This is the interface to the subprocess, accessible by the main
    process."""

    DEVICEMANAGER_RETRY_LIMIT = 8
    DEVICEMANAGER_SETTLING_TIME = 60
    PHONE_RETRY_LIMIT = 2
    PHONE_RETRY_WAIT = 15
    PHONE_MAX_REBOOTS = 3
    PHONE_PING_INTERVAL = 15 * 60
    PHONE_COMMAND_QUEUE_TIMEOUT = 10

    def __init__(self, worker_num, ipaddr, tests, phone_cfg, user_cfg,
                 autophone_queue, logfile_prefix, loglevel, mailer,
                 build_cache_port):
        self.phone_cfg = phone_cfg
        self.user_cfg = user_cfg
        self.worker_num = worker_num
        self.ipaddr = ipaddr
        self.last_status_msg = None
        self.first_status_of_type = None
        self.last_status_of_previous_type = None
        self.crashes = Crashes(crash_window=user_cfg[PHONE_CRASH_WINDOW],
                               crash_limit=user_cfg[PHONE_CRASH_LIMIT])
        self.cmd_queue = multiprocessing.Queue()
        self.lock = multiprocessing.Lock()
        self.subprocess = PhoneWorkerSubProcess(self.worker_num, self.ipaddr,
                                                tests, phone_cfg, user_cfg,
                                                autophone_queue,
                                                self.cmd_queue, logfile_prefix,
                                                loglevel, mailer,
                                                build_cache_port)
        self.logger = logging.getLogger('autophone.worker')
        self.loggerdeco = LogDecorator(self.logger, {
            'phoneid': self.phone_cfg['phoneid'],
            'phoneip': self.phone_cfg['ip']
        }, '%(phoneid)s|%(phoneip)s|%(message)s')

    def is_alive(self):
        return self.subprocess.is_alive()

    def start(self, status=phonetest.PhoneTestMessage.IDLE):
        self.subprocess.start(status)

    def stop(self):
        self.subprocess.stop()

    def new_job(self):
        self.cmd_queue.put_nowait(('job', None))

    def reboot(self):
        self.cmd_queue.put_nowait(('reboot', None))

    def disable(self):
        self.cmd_queue.put_nowait(('disable', None))

    def enable(self):
        self.cmd_queue.put_nowait(('enable', None))

    def debug(self, level):
        try:
            level = int(level)
        except ValueError:
            self.loggerdeco.error('Invalid argument for debug: %s' % level)
        else:
            self.user_cfg['debug'] = level
            self.cmd_queue.put_nowait(('debug', level))

    def ping(self):
        self.cmd_queue.put_nowait(('ping', None))

    def process_msg(self, msg):
        """These are status messages routed back from the autophone_queue
        listener in the main AutoPhone class. There is probably a bit
        clearer way to do this..."""
        if not self.last_status_msg or msg.status != self.last_status_msg.status:
            self.last_status_of_previous_type = self.last_status_msg
            self.first_status_of_type = msg
        self.last_status_msg = msg
Example #19
0
class PhoneTest(object):

    # Use instances keyed on phoneid+':'config_file+':'+str(chunk)
    # to lookup tests.

    instances = {}
    has_run_if_changed = False

    @classmethod
    def lookup(cls, phoneid, config_file, chunk):
        key = '%s:%s:%s' % (phoneid, config_file, chunk)
        if key in PhoneTest.instances:
            return PhoneTest.instances[key]
        return None

    @classmethod
    def match(cls, tests=None, test_name=None, phoneid=None,
              config_file=None, job_guid=None,
              repo=None, platform=None, build_type=None, build_abi=None, build_sdk=None,
              changeset_dirs=None):

        logger = utils.getLogger()
        logger.debug('PhoneTest.match(tests: %s, test_name: %s, phoneid: %s, '
                     'config_file: %s, job_guid: %s, '
                     'repo: %s, platform: %s, build_type: %s, '
                     'abi: %s, build_sdk: %s',
                     tests, test_name, phoneid,
                     config_file, job_guid,
                     repo, platform, build_type, build_abi, build_sdk)
        matches = []
        if not tests:
            tests = [PhoneTest.instances[key] for key in PhoneTest.instances.keys()]

        for test in tests:
            # If changeset_dirs is empty, we will run the tests anyway.
            # This is safer in terms of catching regressions and extra tests
            # being run are more likely to be noticed and fixed than tests
            # not being run that should have been.
            if hasattr(test, 'run_if_changed') and test.run_if_changed and changeset_dirs:
                matched = False
                for cd in changeset_dirs:
                    if matched:
                        break
                    for td in test.run_if_changed:
                        if cd == "" or cd.startswith(td):
                            logger.debug('PhoneTest.match: test %s dir %s '
                                         'matched changeset_dirs %s', test, td, cd)
                            matched = True
                            break
                if not matched:
                    continue

            if test_name and test_name != test.name and \
               "%s%s" % (test_name, test.name_suffix) != test.name:
                continue

            if phoneid and phoneid != test.phone.id:
                continue

            if config_file and config_file != test.config_file:
                continue

            if job_guid and job_guid != test.job_guid:
                continue

            if repo and test.repos and repo not in test.repos:
                continue

            if build_type and build_type not in test.buildtypes:
                continue

            if platform and platform not in test.platforms:
                continue

            if build_abi and build_abi not in test.phone.abi:
                # phone.abi may be of the form armeabi-v7a, arm64-v8a
                # or some form of x86. Test for inclusion rather than
                # exact matches to cover the possibilities.
                continue

            if build_sdk and build_sdk not in test.phone.supported_sdks:
                # We have extended build_sdk and the
                # phone.supported_sdks to be a comma-delimited list of
                # sdk values for phones whose minimum support has
                # changed as the builds have changed.
                sdk_found = False
                for sdk in build_sdk.split(','):
                    if sdk in test.phone.supported_sdks:
                        sdk_found = True
                        break
                if not sdk_found:
                    continue

            matches.append(test)

        logger.debug('PhoneTest.match = %s', matches)

        return matches

    def __init__(self, dm=None, phone=None, options=None, config_file=None, chunk=1, repos=[]):
        # The PhoneTest constructor may raise exceptions due to issues with
        # the device. Creators of PhoneTest objects are responsible
        # for catching these exceptions and cleaning up any previously
        # created tests for the device.
        #
        # Ensure that repos is a list and that it is sorted in order
        # for comparisons with the tests loaded from the jobs database
        # to work.
        assert type(repos) == list, 'PhoneTest repos argument must be a list'
        repos.sort()
        self._add_instance(phone.id, config_file, chunk)
        # The default preferences and environment for running fennec
        # are set here in PhoneTest. Tests which subclass PhoneTest can
        # add or override these preferences during their
        # initialization.
        self._preferences = None
        self._environment = None
        self.config_file = config_file
        self.cfg = ConfigParser.ConfigParser()
        # Make the values in the config file case-sensitive
        self.cfg.optionxform = str
        self.cfg.read(self.config_file)
        self.enable_unittests = False
        self.chunk = chunk
        self.chunks = 1
        self.update_status_cb = None
        self.dm = dm
        self.phone = phone
        self.worker_subprocess = None
        self.options = options

        logger = utils.getLogger(name=self.phone.id)
        self.loggerdeco = LogDecorator(logger,
                                       {},
                                       '%(message)s')
        self.loggerdeco_original = None
        self.dm_logger_original = None
        # Test result
        self.status = TreeherderStatus.SUCCESS
        self.passed = 0
        self.failed = 0
        self.todo = 0

        self.repos = repos
        self.unittest_logpath = None
        # Treeherder related items.
        self._job_name = None
        self._job_symbol = None
        self._group_name = None
        self._group_symbol = None
        self.message = None
        # A unique consistent guid is necessary for identifying the
        # test job in treeherder. The test job_guid is updated when a
        # test is added to the pending jobs/tests in the jobs
        # database.
        self.job_guid = None
        self.job_details = []
        self.submit_timestamp = None
        self.start_timestamp = None
        self.end_timestamp = None
        if not self.cfg.sections():
            self.loggerdeco.warning('Test configuration not found. '
                                    'Will use defaults.')
        # upload_dir will hold ANR traces, tombstones and other files
        # pulled from the device.
        self.upload_dir = None
        # crash_processor is an instance of AutophoneCrashProcessor that
        # is used by non-unittests to process device errors and crashes.
        self.crash_processor = None
        # Instrument running time
        self.start_time = None
        self.stop_time = None
        # Perform initial configuration. For tests which do not
        # specify all config options, reasonable defaults will be
        # chosen.

        # [paths]

        # base_device_path accesses the device to determine the
        # appropriate path and can therefore fail and raise an
        # exception which will not be caught in the PhoneTest
        # constructor. Creators of PhoneTest objects are responsible
        # for catching these exceptions and cleaning up any previously
        # created tests for the device.
        self._base_device_path = ''
        self.profile_path = '/data/local/tests/autophone/profile'
        if self.dm:
            self.profile_path = '%s/profile' % self.base_device_path
        self.autophone_directory = os.path.dirname(os.path.abspath(sys.argv[0]))
        self._paths = {}
        try:
            sources = self.cfg.get('paths', 'sources').split()
            self._paths['sources'] = []
            for source in sources:
                if not source.endswith('/'):
                    source += '/'
                self._paths['sources'].append(source)
        except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
            self._paths['sources'] = ['files/base/']
        try:
            self._paths['dest'] = self.cfg.get('paths', 'dest')
            if not self._paths['dest'].endswith('/'):
                self._paths['dest'] += '/'
        except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
            self._paths['dest'] = os.path.join(self.base_device_path,
                                               self.__class__.__name__)
        try:
            self._paths['profile'] = self.cfg.get('paths', 'profile')
            if not self._paths['profile'].endswith('/'):
                self._paths['profile'] += '/'
        except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
            pass
        self.run_if_changed = set()
        try:
            dirs = self.cfg.get('runtests', 'run_if_changed')
            self.run_if_changed = set([d.strip() for d in dirs.split(',')])
            if self.run_if_changed:
                PhoneTest.has_run_if_changed = True
        except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
            pass
        if 'profile' in self._paths:
            self.profile_path = self._paths['profile']
        # _pushes = {'sourcepath' : 'destpath', ...}
        self._pushes = {}
        for source in self._paths['sources']:
            for push in glob.glob(source + '*'):
                if push.endswith('~') or push.endswith('.bak'):
                    continue
                push_file_name = os.path.basename(push)
                push_dest = posixpath.join(self._paths['dest'],
                                           source,
                                           push_file_name)
                self._pushes[push] = push_dest
                if push_file_name == 'initialize_profile.html':
                    self._initialize_url = 'file://' + push_dest
        # [tests]
        self._tests = {}
        try:
            for t in self.cfg.items('tests'):
                self._tests[t[0]] = t[1]
        except ConfigParser.NoSectionError:
            self._tests['blank'] = 'blank.html'

        # [builds]
        self.buildtypes = []
        try:
            self.buildtypes = self.cfg.get('builds', 'buildtypes').split(' ')
        except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
            self.buildtypes = list(self.options.buildtypes)

        self.platforms = []
        try:
            self.platforms = self.cfg.get('builds', 'platforms').split(' ')
        except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
            self.platforms = self.options.platforms

        self.loggerdeco.info('PhoneTest: %s', self.__dict__)

    def __str__(self):
        return '%s(%s, config_file=%s, chunk=%s, buildtypes=%s)' % (
            type(self).__name__,
            self.phone,
            self.config_file,
            self.chunk,
            self.buildtypes)

    def __repr__(self):
        return self.__str__()

    def _add_instance(self, phoneid, config_file, chunk):
        key = '%s:%s:%s' % (phoneid, config_file, chunk)
        assert key not in PhoneTest.instances, 'Duplicate PhoneTest %s' % key
        PhoneTest.instances[key] = self

    def remove(self):
        key = '%s:%s:%s' % (self.phone.id, self.config_file, self.chunk)
        if key in PhoneTest.instances:
            had_run_if_changed = hasattr(PhoneTest.instances[key], 'run_if_changed') and \
                                 PhoneTest.instances[key].run_if_changed
            del PhoneTest.instances[key]
            if had_run_if_changed:
                PhoneTest.has_run_if_changed = False
                for key in PhoneTest.instances.keys():
                    if PhoneTest.instances[key].run_if_changed:
                        PhoneTest.has_run_if_changed = True
                        break

    def set_worker_subprocess(self, worker_subprocess):
        logger = utils.getLogger()
        self.loggerdeco = LogDecorator(logger,
                                       {},
                                       '%(message)s')

        self.loggerdeco_original = None
        self.dm_logger_original = None
        self.worker_subprocess = worker_subprocess
        self.dm = worker_subprocess.dm
        self.update_status_cb = worker_subprocess.update_status

    @property
    def preferences(self):
        # https://dxr.mozilla.org/mozilla-central/source/mobile/android/app/mobile.js
        # https://dxr.mozilla.org/mozilla-central/source/browser/app/profile/firefox.js
        # https://dxr.mozilla.org/mozilla-central/source/addon-sdk/source/test/preferences/no-connections.json
        # https://dxr.mozilla.org/mozilla-central/source/testing/profiles/prefs_general.js
        # https://dxr.mozilla.org/mozilla-central/source/testing/mozbase/mozprofile/mozprofile/profile.py

        if not self._preferences:
            self._preferences = {
                'app.support.baseURL': 'http://localhost/support-dummy/',
                'app.update.auto': False,
                'app.update.certs.1.commonName': '',
                'app.update.certs.2.commonName': '',
                'app.update.enabled': False,
                'app.update.staging.enabled': False,
                'app.update.url': 'http://localhost/app-dummy/update',
                'app.update.url.android': 'http://localhost/app-dummy/update',
                'beacon.enabled': False,
                'browser.EULA.override': True,
                'browser.aboutHomeSnippets.updateUrl': '',
                'browser.contentHandlers.types.0.uri': 'http://localhost/rss?url=%%s',
                'browser.contentHandlers.types.1.uri': 'http://localhost/rss?url=%%s',
                'browser.contentHandlers.types.2.uri': 'http://localhost/rss?url=%%s',
                'browser.contentHandlers.types.3.uri': 'http://localhost/rss?url=%%s',
                'browser.contentHandlers.types.4.uri': 'http://localhost/rss?url=%%s',
                'browser.contentHandlers.types.5.uri': 'http://localhost/rss?url=%%s',
                'browser.firstrun.show.localepicker': False,
                'browser.firstrun.show.uidiscovery': False,
                'browser.newtab.url': '',
                'browser.newtabpage.directory.ping': '',
                'browser.newtabpage.directory.source': 'data:application/json,{"dummy":1}',
                'browser.newtabpage.remote': False,
                'browser.push.warning.infoURL': 'http://localhost/alerts-dummy/infoURL',
                'browser.push.warning.migrationURL': 'http://localhost/alerts-dummy/migrationURL',
                'browser.safebrowsing.appRepURL': '',
                'browser.safebrowsing.downloads.enabled': False,
                'browser.safebrowsing.downloads.remote.enabled': False,
                'browser.safebrowsing.downloads.remote.url': 'http://localhost/safebrowsing-dummy/update',
                'browser.safebrowsing.enabled': False,
                'browser.safebrowsing.gethashURL': '',
                'browser.safebrowsing.malware.enabled': False,
                'browser.safebrowsing.malware.reportURL': '',
                'browser.safebrowsing.provider.google.appRepURL': 'http://localhost/safebrowsing-dummy/update',
                'browser.safebrowsing.provider.google.gethashURL': 'http://localhost/safebrowsing-dummy/gethash',
                'browser.safebrowsing.provider.google.updateURL': 'http://localhost/safebrowsing-dummy/update',
                'browser.safebrowsing.provider.mozilla.gethashURL': 'http://localhost/safebrowsing-dummy/gethash',
                'browser.safebrowsing.provider.mozilla.updateURL': 'http://localhost/safebrowsing-dummy/update',
                'browser.safebrowsing.updateURL': 'http://localhost/safebrowsing-dummy/update',
                'browser.search.countryCode': 'US',
                'browser.search.geoSpecificDefaults': False,
                'browser.search.geoip.url': '',
                'browser.search.isUS': True,
                'browser.search.suggest.enabled': False,
                'browser.search.update': False,
                'browser.selfsupport.url': 'https://localhost/selfsupport-dummy/',
                'browser.sessionstore.resume_from_crash': False,
                'browser.shell.checkDefaultBrowser': False,
                'browser.snippets.enabled': False,
                'browser.snippets.firstrunHomepage.enabled': False,
                'browser.snippets.syncPromo.enabled': False,
                'browser.snippets.updateUrl': '',
                'browser.tabs.warnOnClose': False,
                'browser.tiles.reportURL': 'http://localhost/tests/robocop/robocop_tiles.sjs',
                'browser.trackingprotection.gethashURL': '',
                'browser.trackingprotection.updateURL': '',
                'browser.translation.bing.authURL': 'http://localhost/browser/browser/components/translation/test/bing.sjs',
                'browser.translation.bing.translateArrayURL': 'http://localhost/browser/browser/components/translation/test/bing.sjs',
                'browser.translation.yandex.translateURLOverride': 'http://localhost/browser/browser/components/translation/test/yandex.sjs',
                'browser.uitour.pinnedTabUrl': 'http://localhost/uitour-dummy/pinnedTab',
                'browser.uitour.url': 'http://localhost/uitour-dummy/tour',
                'browser.urlbar.suggest.searches': False,
                'browser.urlbar.userMadeSearchSuggestionsChoice': True,
                'browser.warnOnQuit': False,
                'browser.webapps.apkFactoryUrl': '',
                'browser.webapps.checkForUpdates': 0,
                'browser.webapps.updateCheckUrl': '',
                'datareporting.healthreport.about.reportUrl': 'http://localhost/abouthealthreport/',
                'datareporting.healthreport.about.reportUrlUnified': 'http://localhost/abouthealthreport/v4/',
                'datareporting.healthreport.documentServerURI': 'http://localhost/healthreport/',
                'datareporting.healthreport.service.enabled': False,
                'datareporting.healthreport.uploadEnabled': False,
                'datareporting.policy.dataSubmissionEnabled': False,
                'datareporting.policy.dataSubmissionPolicyBypassAcceptance': True,
                'dom.ipc.plugins.flash.subprocess.crashreporter.enabled': False,
                'experiments.manifest.uri': 'http://localhost/experiments-dummy/manifest',
                'extensions.autoDisableScopes': 0, # By default don't disable add-ons from any scope
                'extensions.blocklist.enabled': False,
                'extensions.blocklist.interval': 172800,
                'extensions.blocklist.url': 'http://localhost/extensions-dummy/blocklistURL',
                'extensions.enabledScopes': 5,     # By default only load extensions all scopes except temporary.
                'extensions.getAddons.cache.enabled': False,
                'extensions.getAddons.get.url': 'http://localhost/extensions-dummy/repositoryGetURL',
                'extensions.getAddons.getWithPerformance.url': 'http://localhost/extensions-dummy/repositoryGetWithPerformanceURL',
                'extensions.getAddons.search.browseURL': 'http://localhost/extensions-dummy/repositoryBrowseURL',
                'extensions.getAddons.search.url': 'http://localhost/extensions-dummy/repositorySearchURL',
                'extensions.hotfix.url': 'http://localhost/extensions-dummy/hotfixURL',
                'extensions.installDistroAddons': False,
                'extensions.showMismatchUI': False,
                'extensions.startupScanScopes': 5, # And scan for changes at startup
                'extensions.systemAddon.update.url': 'data:application/xml,<updates></updates>',
                'extensions.update.autoUpdateDefault': False,
                'extensions.update.background.url': 'http://localhost/extensions-dummy/updateBackgroundURL',
                'extensions.update.enabled': False,
                'extensions.update.interval': 172800,
                'extensions.update.notifyUser': False,
                'extensions.update.url': 'http://localhost/extensions-dummy/updateURL',
                'extensions.webservice.discoverURL': 'http://localhost/extensions-dummy/discoveryURL',
                'general.useragent.updates.enabled': False,
                'geo.provider.testing': True,
                'geo.wifi.scan': False,
                'geo.wifi.uri': 'http://localhost/tests/dom/tests/mochitest/geolocation/network_geolocation.sjs',
                'identity.fxaccounts.auth.uri': 'https://localhost/fxa-dummy/',
                'identity.fxaccounts.remote.force_auth.uri': 'https://localhost/fxa-force-auth',
                'identity.fxaccounts.remote.signin.uri': 'https://localhost/fxa-signin',
                'identity.fxaccounts.remote.signup.uri': 'https://localhost/fxa-signup',
                'identity.fxaccounts.remote.webchannel.uri': 'https://localhost/',
                'identity.fxaccounts.settings.uri': 'https://localhost/fxa-settings',
                'identity.fxaccounts.skipDeviceRegistration': True,
                'media.autoplay.enabled': True,
                'media.gmp-gmpopenh264.autoupdate': False,
                'media.gmp-manager.cert.checkAttributes': False,
                'media.gmp-manager.cert.requireBuiltIn': False,
                'media.gmp-manager.certs.1.commonName': '',
                'media.gmp-manager.certs.2.commonName': '',
                'media.gmp-manager.secondsBetweenChecks': 172800,
                'media.gmp-manager.url': 'http://localhost/media-dummy/gmpmanager',
                'media.gmp-manager.url.override': 'data:application/xml,<updates></updates>',
                'plugin.state.flash': 2,
                'plugins.update.url': 'http://localhost/plugins-dummy/updateCheckURL',
                'privacy.trackingprotection.introURL': 'http://localhost/trackingprotection/tour',
                'security.notification_enable_delay': 0,
                'security.ssl.errorReporting.url': 'https://localhost/browser/browser/base/content/test/general/pinning_reports.sjs?succeed',
                'shell.checkDefaultClient': False,
                'toolkit.startup.max_resumed_crashes': -1,
                'toolkit.telemetry.cachedClientID': 'dddddddd-dddd-dddd-dddd-dddddddddddd', # https://dxr.mozilla.org/mozilla-central/source/toolkit/modules/ClientID.jsm#40
                'toolkit.telemetry.enabled': False,
                'toolkit.telemetry.notifiedOptOut': 999,
                'toolkit.telemetry.prompted': 999,
                'toolkit.telemetry.rejected': True,
                'toolkit.telemetry.server': 'https://localhost/telemetry-dummy/',
                'toolkit.telemetry.unified': False,
                'urlclassifier.updateinterval': 172800,
                'webapprt.app_update_interval': 172800,
                'xpinstall.signatures.required': False,
                }
            if self.cfg.has_section('preferences'):
                overrides = self.cfg.options('preferences')
                for name in overrides:
                    value = self.cfg.get('preferences', name)
                    if value.lower() == 'true':
                        value = True
                    elif value.lower() == 'false':
                        value = False
                    elif re.match(r'\d+$', value):
                        value = int(value)
                    self._preferences[name] = value
        return self._preferences

    @property
    def environment(self):
        if not self._environment:
            # https://developer.mozilla.org/en-US/docs/Environment_variables_affecting_crash_reporting
            self._environment = {
                'MOZ_CRASHREPORTER': '1',
                'MOZ_CRASHREPORTER_NO_REPORT': '1',
                'MOZ_CRASHREPORTER_SHUTDOWN': '1',
                'MOZ_DISABLE_NONLOCAL_CONNECTIONS': '1',
                'MOZ_IN_AUTOMATION': '1',
                'NO_EM_RESTART': '1',
                'MOZ_DISABLE_SWITCHBOARD': '1',
                #'NSPR_LOG_MODULES': 'all:5',
            }
            if self.cfg.has_section('environment'):
                overrides = self.cfg.options('environment')
                for name in overrides:
                    value = self.cfg.get('environment', name)
                    if value.lower() == 'true':
                        value = True
                    elif value.lower() == 'false':
                        value = False
                    elif re.match(r'\d+$', value):
                        value = int(value)
                    self._environment[name] = value
        return self._environment

    @property
    def name_suffix(self):
        return  '-%s' % self.chunk if self.chunks > 1 else ''

    @property
    def name(self):
        return 'autophone-%s%s' % (self.__class__.__name__, self.name_suffix)

    @property
    def base_device_path(self):
        if self._base_device_path:
            return self._base_device_path
        success = False
        for attempt in range(1, self.options.phone_retry_limit+1):
            try:
                self._base_device_path = self.dm.test_root + '/autophone'
                if not self.dm.is_dir(self._base_device_path, root=True):
                    self.loggerdeco.debug('Attempt %d creating base device path %s',
                                          attempt, self._base_device_path)
                    self.dm.mkdir(self._base_device_path, parents=True, root=True)
                    self.dm.chmod(self._base_device_path, recursive=True, root=True)
                success = True
                break
            except ADBError:
                self.loggerdeco.exception('Attempt %d creating base device '
                                          'path %s',
                                          attempt, self._base_device_path)
                sleep(self.options.phone_retry_wait)

        if not success:
            raise Exception('Failed to determine base_device_path')

        self.loggerdeco.debug('base_device_path is %s', self._base_device_path)

        return self._base_device_path

    @property
    def job_url(self):
        if not self.options.treeherder_url:
            return None
        job_url = ('%s/#/jobs?filter-searchStr=autophone&' +
                   'exclusion_profile=false&' +
                   'filter-tier=1&filter-tier=2&filter-tier=3&' +
                   'repo=%s&revision=%s')
        return job_url % (self.options.treeherder_url,
                          self.build.tree,
                          self.build.revision[:12])

    @property
    def job_name(self):
        if not self.options.treeherder_url:
            return None
        if not self._job_name:
            self._job_name = self.cfg.get('treeherder', 'job_name')
        return self._job_name

    @property
    def job_symbol(self):
        if not self.options.treeherder_url:
            return None
        if not self._job_symbol:
            self._job_symbol = self.cfg.get('treeherder', 'job_symbol')
            if self.chunks > 1:
                self._job_symbol = "%s%s" %(self._job_symbol, self.chunk)
        return self._job_symbol

    @property
    def group_name(self):
        if not self.options.treeherder_url:
            return None
        if not self._group_name:
            self._group_name = self.cfg.get('treeherder', 'group_name')
        return self._group_name

    @property
    def group_symbol(self):
        if not self.options.treeherder_url:
            return None
        if not self._group_symbol:
            self._group_symbol = self.cfg.get('treeherder', 'group_symbol')
        return self._group_symbol

    @property
    def build(self):
        return self.worker_subprocess.build

    def get_test_package_names(self):
        """Return a set of test package names which need to be downloaded
        along with the build in order to run the test. This set will
        be passed to the BuildCache.get() method. Normally, this will
        only need to be set for UnitTests.

        See https://bugzilla.mozilla.org/show_bug.cgi?id=1158276
            https://bugzilla.mozilla.org/show_bug.cgi?id=917999
        """
        return set()

    def generate_guid(self):
        self.job_guid = utils.generate_guid()

    def handle_test_interrupt(self, reason, test_result):
        self.add_failure(self.name, TestStatus.TEST_UNEXPECTED_FAIL, reason, test_result)

    def add_pass(self, testpath):
        testpath = _normalize_testpath(testpath)
        self.passed += 1
        self.loggerdeco.info(' %s | %s | Ok.', TestStatus.TEST_PASS, testpath)

    def add_failure(self, testpath, test_status, text, testresult_status):
        """Report a test failure.

        :param testpath: A string identifying the test.
        :param test_status: A string identifying the type of test failure.
        :param text: A string decribing the failure.
        :param testresult_status: Test status to be reported to Treeherder.
            One of PhoneTest.{BUSTED,EXCEPTION,TESTFAILED,UNKNOWN,USERCANCEL}.
        """
        self.message = text
        self.update_status(message=text)
        testpath = _normalize_testpath(testpath)
        self.status = testresult_status
        self.failed += 1
        self.loggerdeco.info(' %s | %s | %s', test_status, testpath, text)

    def reset_result(self):
        self.status = TreeherderStatus.SUCCESS
        self.passed = 0
        self.failed = 0
        self.todo = 0

    def handle_crashes(self):
        """Detect if any crash dump files have been generated, process them and
        produce Treeherder compatible error messages, then clean up the dump
        files before returning True if a crash was found or False if there were
        no crashes detected.
        """
        if not self.crash_processor:
            return False

        errors = self.crash_processor.get_errors(self.build.symbols,
                                                 self.options.minidump_stackwalk,
                                                 clean=True)

        for error in errors:
            if error['reason'] == 'java-exception':
                self.add_failure(
                    self.name, TestStatus.PROCESS_CRASH,
                    error['signature'],
                    TreeherderStatus.TESTFAILED)
            elif error['reason'] == TestStatus.TEST_UNEXPECTED_FAIL:
                self.add_failure(
                    self.name,
                    error['reason'],
                    error['signature'],
                    TreeherderStatus.TESTFAILED)
            elif error['reason'] == TestStatus.PROCESS_CRASH:
                self.add_failure(
                    self.name,
                    error['reason'],
                    'application crashed [%s]' % error['signature'],
                    TreeherderStatus.TESTFAILED)
                self.loggerdeco.info(error['signature'])
                self.loggerdeco.info(error['stackwalk_output'])
                self.loggerdeco.info(error['stackwalk_errors'])
            else:
                self.loggerdeco.warning('Unknown error reason: %s', error['reason'])

        return len(errors) > 0

    def stop_application(self):
        """Stop the application cleanly.

        Make the home screen active placing the application into the
        background, then attempt to use am force-stop, am kill, or
        shell kill to stop the package in that order.

        Only requires a rooted device if am force-stop and am kill
        both fail.

        Returns True if the process no longer exists.

        Raises ADBError, ADBRootErrro, ADBTimeoutError
        """
        result = True
        self.loggerdeco.debug('stop_application: display home screen')
        self.dm.shell_output("am start "
                             "-a android.intent.action.MAIN "
                             "-c android.intent.category.HOME")
        self.loggerdeco.debug('stop_application: am force-stop')
        self.dm.shell_output("am force-stop %s" % self.build.app_name)
        if self.dm.process_exist(self.build.app_name):
            self.loggerdeco.debug('stop_application: am kill')
            self.dm.shell_output("am kill %s" % self.build.app_name)
            if self.dm.process_exist(self.build.app_name):
                self.loggerdeco.debug('stop_application: kill')
                self.dm.pkill(self.build.app_name, root=True)
                result = not self.dm.process_exist(self.build.app_name)
        return result

    def create_profile(self, custom_addons=[], custom_prefs=None, root=True):
        # Create, install and initialize the profile to be
        # used in the test.

        temp_addons = ['quitter.xpi']
        temp_addons.extend(custom_addons)
        addons = ['%s/xpi/%s' % (os.getcwd(), addon) for addon in temp_addons]

        # make sure firefox isn't running when we try to
        # install the profile.

        self.dm.pkill(self.build.app_name, root=root)
        if isinstance(custom_prefs, dict):
            prefs = dict(self.preferences.items() + custom_prefs.items())
        else:
            prefs = self.preferences
        profile = Profile(preferences=prefs, addons=addons)
        if not self.install_profile(profile):
            return False

        success = False
        for attempt in range(1, self.options.phone_retry_limit+1):
            self.loggerdeco.debug('Attempt %d Initializing profile', attempt)
            self.run_fennec_with_profile(self.build.app_name, self._initialize_url)

            if self.wait_for_fennec():
                success = True
                break
            sleep(self.options.phone_retry_wait)

        if not success or self.handle_crashes():
            self.add_failure(self.name, TestStatus.TEST_UNEXPECTED_FAIL,
                             'Failure initializing profile',
                             TreeherderStatus.TESTFAILED)

        return success

    def wait_for_fennec(self, max_wait_time=60, wait_time=5,
                        kill_wait_time=20, root=True):
        # Wait for up to a max_wait_time seconds for fennec to close
        # itself in response to the quitter request. Check that fennec
        # is still running every wait_time seconds. If fennec doesn't
        # close on its own, attempt up to 3 times to kill fennec, waiting
        # kill_wait_time seconds between attempts.
        # Return True if fennec exits on its own, False if it needs to be killed.
        # Re-raise the last exception if fennec can not be killed.
        max_wait_attempts = max_wait_time / wait_time
        for wait_attempt in range(1, max_wait_attempts+1):
            if not self.dm.process_exist(self.build.app_name):
                return True
            sleep(wait_time)
        max_killattempts = 3
        for kill_attempt in range(1, max_killattempts+1):
            try:
                self.loggerdeco.info('killing %s' % self.build.app_name)
                self.stop_application()
                break
            except ADBError:
                self.loggerdeco.exception('Attempt %d to kill fennec failed' %
                                          kill_attempt)
                if kill_attempt == max_killattempts:
                    raise
                sleep(kill_wait_time)
        return False

    def install_local_pages(self):
        success = False
        for attempt in range(1, self.options.phone_retry_limit+1):
            self.loggerdeco.debug('Attempt %d Installing local pages', attempt)
            try:
                self.dm.rm(self._paths['dest'], recursive=True, force=True, root=True)
                self.dm.mkdir(self._paths['dest'], parents=True, root=True)
                for push_source in self._pushes:
                    push_dest = self._pushes[push_source]
                    if os.path.isdir(push_source):
                        self.dm.push(push_source, push_dest)
                    else:
                        self.dm.push(push_source, push_dest)
                self.dm.chmod(self._paths['dest'], recursive=True, root=True)
                success = True
                break
            except ADBError:
                self.loggerdeco.exception('Attempt %d Installing local pages', attempt)
                sleep(self.options.phone_retry_wait)

        if not success:
            self.add_failure(self.name, TestStatus.TEST_UNEXPECTED_FAIL,
                             'Failure installing local pages',
                             TreeherderStatus.TESTFAILED)

        return success

    def is_fennec_running(self, appname):
        for attempt in range(1, self.options.phone_retry_limit+1):
            try:
                return self.dm.process_exist(appname)
            except ADBError:
                self.loggerdeco.exception('Attempt %d is fennec running', attempt)
                if attempt == self.options.phone_retry_limit:
                    raise
                sleep(self.options.phone_retry_wait)

    def setup_job(self):
        # Truncate the current log to prevent log bleed over. See the
        # main process log for all of the mising bits. Log the current
        # full contents of logcat, then clear the logcat buffers to
        # help prevent the device's buffer from over flowing during
        # the test.
        self.worker_subprocess.filehandler.stream.truncate(0)
        self.worker_subprocess.log_step('Setup Test')
        self.start_time = datetime.datetime.utcnow()
        self.stop_time = self.start_time
        # Clear the Treeherder job details.
        self.job_details = []
        try:
            self.worker_subprocess.logcat.reset()
        except:
            self.loggerdeco.exception('Exception resetting logcat before test')
        self.worker_subprocess.treeherder.submit_running(
            self.phone.id,
            self.build.url,
            self.build.tree,
            self.build.revision,
            self.build.type,
            self.build.abi,
            self.build.platform,
            self.build.sdk,
            self.build.builder_type,
            tests=[self])

        self.loggerdeco_original = self.loggerdeco
        # self.dm._logger can raise ADBTimeoutError due to the
        # property dm therefore place it after the initialization.
        self.dm_logger_original = self.dm._logger
        logger = utils.getLogger()
        self.loggerdeco = LogDecorator(logger,
                                       {
                                           'repo': self.build.tree,
                                           'buildid': self.build.id,
                                           'buildtype': self.build.type,
                                           'sdk': self.phone.sdk,
                                           'platform': self.build.platform,
                                           'testname': self.name
                                       },
                                       'PhoneTestJob %(repo)s %(buildid)s %(buildtype)s %(sdk)s %(platform)s %(testname)s '
                                       '%(message)s')
        self.dm._logger = self.loggerdeco
        self.loggerdeco.info('PhoneTest starting job')
        if self.unittest_logpath:
            os.unlink(self.unittest_logpath)
        self.unittest_logpath = None
        self.upload_dir = tempfile.mkdtemp()
        self.crash_processor = AutophoneCrashProcessor(self.dm,
                                                       self.profile_path,
                                                       self.upload_dir,
                                                       self.build.app_name)
        self.crash_processor.clear()
        self.reset_result()
        if not self.worker_subprocess.is_disabled():
            self.update_status(phone_status=PhoneStatus.WORKING,
                               message='Setting up %s' % self.name)

    def run_job(self):
        raise NotImplementedError

    def teardown_job(self):
        self.loggerdeco.debug('PhoneTest.teardown_job')
        self.stop_time = datetime.datetime.utcnow()
        if self.stop_time and self.start_time:
            self.loggerdeco.info('Test %s elapsed time: %s',
                                 self.name, self.stop_time - self.start_time)
        try:
            if self.worker_subprocess.is_ok():
                # Do not attempt to process crashes if the device is
                # in an error state.
                self.handle_crashes()
        except Exception, e:
            self.loggerdeco.exception('Exception during crash processing')
            self.add_failure(
                self.name, TestStatus.TEST_UNEXPECTED_FAIL,
                'Exception %s during crash processing' % e,
                TreeherderStatus.EXCEPTION)
        if self.unittest_logpath and os.path.exists(self.unittest_logpath):
            self.worker_subprocess.log_step('Unittest Log')
            try:
                logfilehandle = open(self.unittest_logpath)
                for logline in logfilehandle.read().splitlines():
                    self.loggerdeco.info(logline)
                logfilehandle.close()
            except:
                self.loggerdeco.exception('Exception loading log %s', self.unittest_log)
            finally:
                os.unlink(self.unittest_logpath)
                self.unittest_logpath = None
        # Unit tests may include the logcat output already but not all of them do.
        self.worker_subprocess.log_step('Logcat')
        try:
            for logcat_line in self.worker_subprocess.logcat.get(full=True):
                self.loggerdeco.info("logcat: %s", logcat_line)
        except:
            self.loggerdeco.exception('Exception getting logcat')
        try:
            if self.worker_subprocess.is_disabled() and self.status != TreeherderStatus.USERCANCEL:
                # The worker was disabled while running one test of a job.
                # Record the cancellation on any remaining tests in that job.
                self.add_failure(
                    self.name, TestStatus.TEST_UNEXPECTED_FAIL,
                    'The worker was disabled.',
                    TreeherderStatus.USERCANCEL)
            self.loggerdeco.info('PhoneTest stopping job')
            self.worker_subprocess.flush_log()
            self.worker_subprocess.treeherder.submit_complete(
                self.phone.id,
                self.build.url,
                self.build.tree,
                self.build.revision,
                self.build.type,
                self.build.abi,
                self.build.platform,
                self.build.sdk,
                self.build.builder_type,
                tests=[self])
        except:
            self.loggerdeco.exception('Exception tearing down job')
        finally:
            if self.upload_dir and os.path.exists(self.upload_dir):
                shutil.rmtree(self.upload_dir)
            self.upload_dir = None

        # Reset the tests' volatile members in order to prevent them
        # from being reused after a test has completed.
        self.message = None
        self.job_guid = None
        self.job_details = []
        self.submit_timestamp = None
        self.start_timestamp = None
        self.end_timestamp = None
        self.upload_dir = None
        self.start_time = None
        self.stop_time = None
        self.unittest_logpath = None
        # Reset the logcat buffers to help prevent the device's buffer
        # from over flowing after the test.
        self.worker_subprocess.logcat.reset()
        if self.loggerdeco_original:
            self.loggerdeco = self.loggerdeco_original
            self.loggerdeco_original = None
        if self.dm_logger_original:
            self.dm._logger = self.dm_logger_original
            self.dm_logger_original = None
        self.reset_result()
Example #20
0
class PhoneTest(object):
    # Use instances keyed on phoneid+':'config_file+':'+str(chunk)
    # to lookup tests.

    instances = {}

    @classmethod
    def lookup(cls, phoneid, config_file, chunk):
        key = '%s:%s:%s' % (phoneid, config_file, chunk)
        if key in PhoneTest.instances:
            return PhoneTest.instances[key]
        return None

    @classmethod
    def match(cls, tests=None, test_name=None, phoneid=None,
              config_file=None, chunk=None, job_guid=None,
              build_url=None):

        logger.debug('PhoneTest.match(tests: %s, test_name: %s, phoneid: %s, '
                     'config_file: %s, chunk: %s, job_guid: %s, '
                     'build_url: %s' % (tests, test_name, phoneid,
                                        config_file, chunk, job_guid,
                                        build_url))
        matches = []
        if not tests:
            tests = [PhoneTest.instances[key] for key in PhoneTest.instances.keys()]

        for test in tests:
            if test_name and test_name != test.name:
                continue

            if phoneid and phoneid != test.phone.id:
                continue

            if config_file and config_file != test.config_file:
                continue

            if chunk and chunk != test.chunk:
                continue

            if job_guid and job_guid != test.job_guid:
                continue

            if build_url:
                abi = test.phone.abi
                sdk = test.phone.sdk
                # First assume the test and build are compatible.
                incompatible_job = False
                # x86 devices can only test x86 builds and non-x86
                # devices can not test x86 builds.
                if abi == 'x86':
                    if 'x86' not in build_url:
                        incompatible_job = True
                else:
                    if 'x86' in build_url:
                        incompatible_job = True
                # If the build_url does not contain an sdk level, then
                # assume this is an build from before the split sdk
                # builds were first created. Otherwise the build_url
                # must match this device's supported sdk levels.
                if ('api-9' not in build_url and 'api-10' not in build_url and
                    'api-11' not in build_url):
                    pass
                elif sdk not in build_url:
                    incompatible_job  = True

                if incompatible_job:
                    continue

                # The test may be defined for multiple repositories.
                # We are interested if this particular build is
                # supported by this test. First assume it is
                # incompatible, and only accept it if the build_url is
                # from one of the supported repositories.
                if test.repos:
                    incompatible_job = True
                    for repo in test.repos:
                        if repo in build_url:
                            incompatible_job = False
                            break
                    if incompatible_job:
                        continue

            matches.append(test)

        logger.debug('PhoneTest.match = %s' % matches)

        return matches

    def __init__(self, dm=None, phone=None, options=None, config_file=None, chunk=1, repos=[]):
        # Ensure that repos is a list and that it is sorted in order
        # for comparisons with the tests loaded from the jobs database
        # to work.
        assert type(repos) == list, 'PhoneTest repos argument must be a list'
        repos.sort()
        self._add_instance(phone.id, config_file, chunk)
        # The default preferences and environment for running fennec
        # are set here in PhoneTest. Tests which subclass PhoneTest can
        # add or override these preferences during their
        # initialization.
        self._preferences = None
        self._environment = None
        self.config_file = config_file
        self.cfg = ConfigParser.ConfigParser()
        # Make the values in the config file case-sensitive
        self.cfg.optionxform = str
        self.cfg.read(self.config_file)
        self.enable_unittests = False
        self.chunk = chunk
        self.chunks = 1
        self.update_status_cb = None
        self.dm = dm
        self.phone = phone
        self.worker_subprocess = None
        self.options = options
        self.loggerdeco = LogDecorator(logger,
                                       {'phoneid': self.phone.id},
                                       '%(phoneid)s|%(message)s')
        self.loggerdeco_original = None
        self.dm_logger_original = None
        self.loggerdeco.info('init autophone.phonetest')
        # test_logfilehandler is used by running tests to save log
        # messages to a separate file which can be reset at the
        # beginning of each test independently of the worker's log.
        self.test_logfilehandler = None
        self._base_device_path = ''
        self.profile_path = '/data/local/tmp/profile'
        self.repos = repos
        self.test_logfile = None
        self.unittest_logpath = None
        # Treeherder related items.
        self._job_name = None
        self._job_symbol = None
        self._group_name = None
        self._group_symbol = None
        self.test_result = PhoneTestResult()
        self.message = None
        # A unique consistent guid is necessary for identifying the
        # test job in treeherder. The test job_guid is updated when a
        # test is added to the pending jobs/tests in the jobs
        # database.
        self.job_guid = None
        self.job_details = []
        self.submit_timestamp = None
        self.start_timestamp = None
        self.end_timestamp = None
        self.logcat = Logcat(self, self.loggerdeco)
        self.loggerdeco.debug('PhoneTest: %s, cfg sections: %s' % (self.__dict__, self.cfg.sections()))
        if not self.cfg.sections():
            self.loggerdeco.warning('Test configuration not found. '
                                    'Will use defaults.')
        # upload_dir will hold ANR traces, tombstones and other files
        # pulled from the device.
        self.upload_dir = None
        # crash_processor is an instance of AutophoneCrashProcessor that
        # is used by non-unittests to process device errors and crashes.
        self.crash_processor = None
        # Instrument running time
        self.start_time = None
        self.stop_time = None
        # Perform initial configuration. For tests which do not
        # specify all config options, reasonable defaults will be
        # chosen.

        # [paths]
        self.autophone_directory = os.path.dirname(os.path.abspath(sys.argv[0]))
        self._paths = {}
        self._paths['dest'] = posixpath.join(self.base_device_path,
                                             self.__class__.__name__)
        try:
            sources = self.cfg.get('paths', 'sources').split()
            self._paths['sources'] = []
            for source in sources:
                if not source.endswith('/'):
                    source += '/'
                self._paths['sources'].append(source)
        except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
            self._paths['sources'] = [
                os.path.join(self.autophone_directory, 'files/base/')]
        try:
            self._paths['dest'] = self.cfg.get('paths', 'dest')
            if not self._paths['dest'].endswith('/'):
                self._paths['dest'] += '/'
        except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
            pass
        try:
            self._paths['profile'] = self.cfg.get('paths', 'profile')
            if not self._paths['profile'].endswith('/'):
                self._paths['profile'] += '/'
        except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
            pass
        if 'profile' in self._paths:
            self.profile_path = self._paths['profile']
        # _pushes = {'sourcepath' : 'destpath', ...}
        self._pushes = {}
        for source in self._paths['sources']:
            for push in glob.glob(source + '*'):
                if push.endswith('~') or push.endswith('.bak'):
                    continue
                push_dest = posixpath.join(self._paths['dest'],
                                           os.path.basename(push))
                self._pushes[push] = push_dest
        self._initialize_url = os.path.join('file://', self._paths['dest'],
                                            'initialize_profile.html')
        # [tests]
        self._tests = {}
        try:
            for t in self.cfg.items('tests'):
                self._tests[t[0]] = t[1]
        except ConfigParser.NoSectionError:
            self._tests['blank'] = 'blank.html'

        self.loggerdeco.info('PhoneTest: Connected.')

    def __str__(self):
        return '%s(%s, config_file=%s, chunk=%s)' % (type(self).__name__,
                                                     self.phone,
                                                     self.config_file,
                                                     self.chunk)

    def __repr__(self):
        return self.__str__()

    def _add_instance(self, phoneid, config_file, chunk):
        key = '%s:%s:%s' % (phoneid, config_file, chunk)
        assert key not in PhoneTest.instances, 'Duplicate PhoneTest %s' % key
        PhoneTest.instances[key] = self

    def remove(self):
        key = '%s:%s:%s' % (self.phone.id, self.config_file, self.chunk)
        if key in PhoneTest.instances:
            del PhoneTest.instances[key]

    @property
    def preferences(self):
        # https://dxr.mozilla.org/mozilla-central/source/mobile/android/app/mobile.js
        # https://dxr.mozilla.org/mozilla-central/source/browser/app/profile/firefox.js
        # https://dxr.mozilla.org/mozilla-central/source/addon-sdk/source/test/preferences/no-connections.json
        # https://dxr.mozilla.org/mozilla-central/source/testing/profiles/prefs_general.js

        if not self._preferences:
            self._preferences = {
                'app.update.auto': False,
                'app.update.certs.1.commonName': '',
                'app.update.certs.2.commonName': '',
                'app.update.enabled': False,
                'app.update.staging.enabled': False,
                'app.update.url': 'http://localhost/app-dummy/update',
                'app.update.url.android': 'http://localhost/app-dummy/update',
                'beacon.enabled': False,
                'browser.EULA.override': True,
                'browser.aboutHomeSnippets.updateUrl': '',
                'browser.contentHandlers.types.0.uri': 'http://localhost/rss?url=%%s',
                'browser.contentHandlers.types.1.uri': 'http://localhost/rss?url=%%s',
                'browser.contentHandlers.types.2.uri': 'http://localhost/rss?url=%%s',
                'browser.contentHandlers.types.3.uri': 'http://localhost/rss?url=%%s',
                'browser.contentHandlers.types.4.uri': 'http://localhost/rss?url=%%s',
                'browser.contentHandlers.types.5.uri': 'http://localhost/rss?url=%%s',
                'browser.firstrun.show.localepicker': False,
                'browser.firstrun.show.uidiscovery': False,
                'browser.newtab.url': '',
                'browser.newtabpage.directory.ping': '',
                'browser.newtabpage.directory.source': 'data:application/json,{"dummy":1}',
                'browser.safebrowsing.appRepURL': '',
                'browser.safebrowsing.downloads.enabled': False,
                'browser.safebrowsing.downloads.remote.enabled': False,
                'browser.safebrowsing.enabled': False,
                'browser.safebrowsing.gethashURL': '',
                'browser.safebrowsing.malware.enabled': False,
                'browser.safebrowsing.malware.reportURL': '',
                'browser.safebrowsing.provider.google.appRepURL': 'http://localhost/safebrowsing-dummy/update',
                'browser.safebrowsing.provider.google.gethashURL': 'http://localhost/safebrowsing-dummy/gethash',
                'browser.safebrowsing.provider.google.updateURL': 'http://localhost/safebrowsing-dummy/update',
                'browser.safebrowsing.provider.mozilla.gethashURL': 'http://localhost/safebrowsing-dummy/gethash',
                'browser.safebrowsing.provider.mozilla.updateURL': 'http://localhost/safebrowsing-dummy/update',
                'browser.safebrowsing.updateURL': 'http://localhost/safebrowsing-dummy/update',
                'browser.search.countryCode': 'US',
                'browser.search.geoSpecificDefaults': False,
                'browser.search.geoip.url': '',
                'browser.search.isUS': True,
                'browser.search.suggest.enabled': False,
                'browser.search.update': False,
                'browser.selfsupport.url': 'https://localhost/selfsupport-dummy/',
                'browser.sessionstore.resume_from_crash': False,
                'browser.snippets.enabled': False,
                'browser.snippets.firstrunHomepage.enabled': False,
                'browser.snippets.syncPromo.enabled': False,
                'browser.snippets.updateUrl': '',
                'browser.tiles.reportURL': 'http://localhost/tests/robocop/robocop_tiles.sjs',
                'browser.trackingprotection.gethashURL': '',
                'browser.trackingprotection.updateURL': '',
                'browser.translation.bing.authURL': 'http://localhost/browser/browser/components/translation/test/bing.sjs',
                'browser.translation.bing.translateArrayURL': 'http://localhost/browser/browser/components/translation/test/bing.sjs',
                'browser.translation.yandex.translateURLOverride': 'http://localhost/browser/browser/components/translation/test/yandex.sjs',
                'browser.uitour.pinnedTabUrl': 'http://localhost/uitour-dummy/pinnedTab',
                'browser.uitour.url': 'http://localhost/uitour-dummy/tour',
                'browser.warnOnQuit': False,
                'browser.webapps.apkFactoryUrl': '',
                'browser.webapps.checkForUpdates': 0,
                'browser.webapps.updateCheckUrl': '',
                'datareporting.healthreport.about.reportUrl': 'http://localhost/abouthealthreport/',
                'datareporting.healthreport.about.reportUrlUnified': 'http://localhost/abouthealthreport/v4/',
                'datareporting.healthreport.documentServerURI': 'http://localhost/healthreport/',
                'datareporting.healthreport.service.enabled': False,
                'datareporting.healthreport.uploadEnabled': False,
                'datareporting.policy.dataSubmissionEnabled': False,
                'datareporting.policy.dataSubmissionPolicyBypassAcceptance': True,
                'dom.ipc.plugins.flash.subprocess.crashreporter.enabled': False,
                'experiments.manifest.uri': 'http://localhost/experiments-dummy/manifest',
                'extensions.autoDisableScopes': 0,
                'extensions.blocklist.enabled': False,
                'extensions.blocklist.interval': 172800,
                'extensions.blocklist.url': 'http://localhost/extensions-dummy/blocklistURL',
                'extensions.enabledScopes': 5,
                'extensions.getAddons.cache.enabled': False,
                'extensions.getAddons.get.url': 'http://localhost/extensions-dummy/repositoryGetURL',
                'extensions.getAddons.getWithPerformance.url': 'http://localhost/extensions-dummy/repositoryGetWithPerformanceURL',
                'extensions.getAddons.search.browseURL': 'http://localhost/extensions-dummy/repositoryBrowseURL',
                'extensions.getAddons.search.url': 'http://localhost/extensions-dummy/repositorySearchURL',
                'extensions.hotfix.url': 'http://localhost/extensions-dummy/hotfixURL',
                'extensions.update.autoUpdateDefault': False,
                'extensions.update.background.url': 'http://localhost/extensions-dummy/updateBackgroundURL',
                'extensions.update.enabled': False,
                'extensions.update.interval': 172800,
                'extensions.update.url': 'http://localhost/extensions-dummy/updateURL',
                'extensions.webservice.discoverURL': 'http://localhost/extensions-dummy/discoveryURL',
                'general.useragent.updates.enabled': False,
                'geo.wifi.scan': False,
                'geo.wifi.uri': 'http://localhost/tests/dom/tests/mochitest/geolocation/network_geolocation.sjs',
                'identity.fxaccounts.auth.uri': 'https://localhost/fxa-dummy/',
                'identity.fxaccounts.remote.force_auth.uri': 'https://localhost/fxa-force-auth',
                'identity.fxaccounts.remote.signin.uri': 'https://localhost/fxa-signin',
                'identity.fxaccounts.remote.signup.uri': 'https://localhost/fxa-signup',
                'identity.fxaccounts.remote.webchannel.uri': 'https://localhost/',
                'identity.fxaccounts.settings.uri': 'https://localhost/fxa-settings',
                'media.autoplay.enabled': True,
                'media.gmp-gmpopenh264.autoupdate': False,
                'media.gmp-manager.cert.checkAttributes': False,
                'media.gmp-manager.cert.requireBuiltIn': False,
                'media.gmp-manager.certs.1.commonName': '',
                'media.gmp-manager.certs.2.commonName': '',
                'media.gmp-manager.secondsBetweenChecks': 172800,
                'media.gmp-manager.url': 'http://localhost/media-dummy/gmpmanager',
                'media.gmp-manager.url.override': 'data:application/xml,<updates></updates>',
                'plugin.state.flash': 2,
                'plugins.update.url': 'http://localhost/plugins-dummy/updateCheckURL',
                'privacy.trackingprotection.introURL': 'http://localhost/trackingprotection/tour',
                'security.ssl.errorReporting.url': 'https://localhost/browser/browser/base/content/test/general/pinning_reports.sjs?succeed',
                'shell.checkDefaultClient': False,
                'toolkit.telemetry.cachedClientID': 'dddddddd-dddd-dddd-dddd-dddddddddddd', # https://dxr.mozilla.org/mozilla-central/source/toolkit/modules/ClientID.jsm#40
                'toolkit.telemetry.enabled': False,
                'toolkit.telemetry.notifiedOptOut': 999,
                'toolkit.telemetry.prompted': 999,
                'toolkit.telemetry.rejected': True,
                'toolkit.telemetry.server': 'https://localhost/telemetry-dummy/',
                'toolkit.telemetry.unified': False,
                'urlclassifier.updateinterval': 172800,
                'webapprt.app_update_interval': 172800,
                'xpinstall.signatures.required': False,
                }
            if self.cfg.has_section('preferences'):
                overrides = self.cfg.options('preferences')
                for name in overrides:
                    value = self.cfg.get('preferences', name)
                    if value.lower() == 'true':
                        value = True
                    elif value.lower() == 'false':
                        value = False
                    elif re.match('\d+$', value):
                        value = int(value)
                    self._preferences[name] = value
        return self._preferences

    @property
    def environment(self):
        if not self._environment:
            # https://developer.mozilla.org/en-US/docs/Environment_variables_affecting_crash_reporting
            self._environment = {
                'MOZ_CRASHREPORTER': '1',
                'MOZ_CRASHREPORTER_NO_REPORT': '1',
                'MOZ_CRASHREPORTER_SHUTDOWN': '1',
                'MOZ_DISABLE_NONLOCAL_CONNECTIONS': '1',
                'NO_EM_RESTART': '1',
                #'NSPR_LOG_MODULES': 'all:5',
            }
            if self.cfg.has_section('environment'):
                overrides = self.cfg.options('environment')
                for name in overrides:
                    value = self.cfg.get('environment', name)
                    if value.lower() == 'true':
                        value = True
                    elif value.lower() == 'false':
                        value = False
                    elif re.match('\d+$', value):
                        value = int(value)
                    self._environment[name] = value
        return self._environment

    @property
    def name_suffix(self):
        return  '-%s' % self.chunk if self.chunks > 1 else ''

    @property
    def name(self):
        return 'autophone-%s%s' % (self.__class__.__name__, self.name_suffix)

    @property
    def base_device_path(self):
        if self._base_device_path:
            return self._base_device_path
        success = False
        e = None
        for attempt in range(1, self.options.phone_retry_limit+1):
            self._base_device_path = self.dm.test_root + '/autophone'
            self.loggerdeco.debug('Attempt %d creating base device path %s' % (
                attempt, self._base_device_path))
            try:
                if not self.dm.is_dir(self._base_device_path):
                    self.dm.mkdir(self._base_device_path, parents=True)
                success = True
                break
            except ADBError:
                self.loggerdeco.exception('Attempt %d creating base device '
                                          'path %s' % (
                                              attempt, self._base_device_path))
                sleep(self.options.phone_retry_wait)

        if not success:
            raise e

        self.loggerdeco.debug('base_device_path is %s' % self._base_device_path)

        return self._base_device_path

    @property
    def job_url(self):
        if not self.options.treeherder_url:
            return None
        job_url = '%s/#/jobs?filter-searchStr=autophone&exclusion_profile=false&repo=%s&revision=%s'
        return job_url % (self.options.treeherder_url,
                          self.build.tree,
                          os.path.basename(self.build.revision)[:12])

    @property
    def job_name(self):
        if not self.options.treeherder_url:
            return None
        if not self._job_name:
            self._job_name = self.cfg.get('treeherder', 'job_name')
        return self._job_name

    @property
    def job_symbol(self):
        if not self.options.treeherder_url:
            return None
        if not self._job_symbol:
            self._job_symbol = self.cfg.get('treeherder', 'job_symbol')
            if self.chunks > 1:
                self._job_symbol = "%s%s" %(self._job_symbol, self.chunk)
        return self._job_symbol

    @property
    def group_name(self):
        if not self.options.treeherder_url:
            return None
        if not self._group_name:
            self._group_name = self.cfg.get('treeherder', 'group_name')
        return self._group_name

    @property
    def group_symbol(self):
        if not self.options.treeherder_url:
            return None
        if not self._group_symbol:
            self._group_symbol = self.cfg.get('treeherder', 'group_symbol')
        return self._group_symbol

    @property
    def build(self):
        return self.worker_subprocess.build

    def get_test_package_names(self):
        """Return a set of test package names which need to be downloaded
        along with the build in order to run the test. This set will
        be passed to the BuildCache.get() method. Normally, this will
        only need to be set for UnitTests.

        See https://bugzilla.mozilla.org/show_bug.cgi?id=1158276
            https://bugzilla.mozilla.org/show_bug.cgi?id=917999
        """
        return set()

    def generate_guid(self):
        self.job_guid = utils.generate_guid()

    def get_buildername(self, tree):
        return "%s %s opt %s" % (
            self.phone.platform, tree, self.name)

    def handle_test_interrupt(self, reason, test_result):
        self.test_failure(self.name, 'TEST-UNEXPECTED-FAIL', reason, test_result)

    def test_pass(self, testpath):
        self.test_result.add_pass(testpath)

    def test_failure(self, testpath, status, message, testresult_status):
        self.message = message
        self.update_status(message=message)
        self.test_result.add_failure(testpath, status, message, testresult_status)

    def handle_crashes(self):
        if not self.crash_processor:
            return

        for error in self.crash_processor.get_errors(self.build.symbols,
                                                     self.options.minidump_stackwalk,
                                                     clean=False):
            if error['reason'] == 'java-exception':
                self.test_failure(
                    self.name, 'PROCESS-CRASH',
                    error['signature'],
                    PhoneTestResult.EXCEPTION)
            elif error['reason'] == 'PROFILE-ERROR':
                self.test_failure(
                    self.name,
                    error['reason'],
                    error['signature'],
                    PhoneTestResult.TESTFAILED)
            elif error['reason'] == 'PROCESS-CRASH':
                self.loggerdeco.info("PROCESS-CRASH | %s | "
                                     "application crashed [%s]" % (self.name,
                                                                   error['signature']))
                self.loggerdeco.info(error['stackwalk_output'])
                self.loggerdeco.info(error['stackwalk_errors'])

                self.test_failure(self.name,
                                  error['reason'],
                                  'application crashed [%s]' % error['signature'],
                                  PhoneTestResult.TESTFAILED)
            else:
                self.loggerdeco.warning('Unknown error reason: %s' % error['reason'])

    def create_profile(self, custom_addons=[], custom_prefs=None, root=True):
        # Create, install and initialize the profile to be
        # used in the test.

        temp_addons = ['quitter.xpi']
        temp_addons.extend(custom_addons)
        addons = ['%s/xpi/%s' % (os.getcwd(), addon) for addon in temp_addons]

        # make sure firefox isn't running when we try to
        # install the profile.

        self.dm.pkill(self.build.app_name, root=root)
        if isinstance(custom_prefs, dict):
            prefs = dict(self.preferences.items() + custom_prefs.items())
        else:
            prefs = self.preferences
        profile = FirefoxProfile(preferences=prefs, addons=addons)
        if not self.install_profile(profile):
            return False

        success = False
        for attempt in range(1, self.options.phone_retry_limit+1):
            self.loggerdeco.debug('Attempt %d Initializing profile' % attempt)
            self.run_fennec_with_profile(self.build.app_name, self._initialize_url)

            if self.wait_for_fennec():
                success = True
                break
            sleep(self.options.phone_retry_wait)

        if not success:
            msg = 'Aborting Test - Failure initializing profile.'
            self.loggerdeco.error(msg)

        return success

    def wait_for_fennec(self, max_wait_time=60, wait_time=5,
                        kill_wait_time=20, root=True):
        # Wait for up to a max_wait_time seconds for fennec to close
        # itself in response to the quitter request. Check that fennec
        # is still running every wait_time seconds. If fennec doesn't
        # close on its own, attempt up to 3 times to kill fennec, waiting
        # kill_wait_time seconds between attempts.
        # Return True if fennec exits on its own, False if it needs to be killed.
        # Re-raise the last exception if fennec can not be killed.
        max_wait_attempts = max_wait_time / wait_time
        for wait_attempt in range(1, max_wait_attempts+1):
            if not self.dm.process_exist(self.build.app_name):
                return True
            sleep(wait_time)
        self.loggerdeco.debug('killing fennec')
        max_killattempts = 3
        for kill_attempt in range(1, max_killattempts+1):
            try:
                self.dm.pkill(self.build.app_name, root=root)
                break
            except ADBError:
                self.loggerdeco.exception('Attempt %d to kill fennec failed' %
                                          kill_attempt)
                if kill_attempt == max_killattempts:
                    raise
                sleep(kill_wait_time)
        return False

    def install_local_pages(self):
        success = False
        for attempt in range(1, self.options.phone_retry_limit+1):
            self.loggerdeco.debug('Attempt %d Installing local pages' % attempt)
            try:
                self.dm.rm(self._paths['dest'], recursive=True, force=True)
                self.dm.mkdir(self._paths['dest'], parents=True)
                for push_source in self._pushes:
                    push_dest = self._pushes[push_source]
                    if os.path.isdir(push_source):
                        self.dm.push(push_source, push_dest)
                    else:
                        self.dm.push(push_source, push_dest)
                success = True
                break
            except ADBError:
                self.loggerdeco.exception('Attempt %d Installing local pages' % attempt)
                sleep(self.options.phone_retry_wait)

        if not success:
            self.loggerdeco.error('Failure installing local pages')

        return success

    def is_fennec_running(self, appname):
        for attempt in range(1, self.options.phone_retry_limit+1):
            try:
                return self.dm.process_exist(appname)
            except ADBError:
                self.loggerdeco.exception('Attempt %d is fennec running' % attempt)
                if attempt == self.options.phone_retry_limit:
                    raise
                sleep(self.options.phone_retry_wait)

    def setup_job(self):
        # Log the current full contents of logcat, then clear the
        # logcat buffers to help prevent the device's buffer from
        # over flowing during the test.
        self.start_time = datetime.datetime.now()
        self.stop_time = self.start_time
        # Clear the Treeherder job details.
        self.job_details = []
        self.loggerdeco.debug('phonetest.setup_job: full logcat before job:')
        try:
            self.loggerdeco.debug('\n'.join(self.logcat.get(full=True)))
        except:
            self.loggerdeco.exception('Exception getting logcat')
        try:
            self.logcat.reset()
        except:
            self.loggerdeco.exception('Exception resetting logcat')
        self.worker_subprocess.treeherder.submit_running(
            self.phone.id,
            self.build.url,
            self.build.tree,
            self.build.revision_hash,
            tests=[self])

        self.loggerdeco_original = self.loggerdeco
        # self.dm._logger can raise ADBTimeoutError due to the
        # property dm therefore place it after the initialization.
        self.dm_logger_original = self.dm._logger
        # Create a test run specific logger which will propagate to
        # the root logger in the worker which runs in the same
        # process. This log will be uploaded to Treeherder if
        # Treeherder submission is enabled and will be cleared at the
        # beginning of each test run.
        sensitive_data_filter = SensitiveDataFilter(self.options.sensitive_data)
        logger = logging.getLogger('phonetest')
        logger.addFilter(sensitive_data_filter)
        logger.propagate = True
        logger.setLevel(self.worker_subprocess.loglevel)
        self.test_logfile = (self.worker_subprocess.logfile_prefix +
                             '-' + self.name + '.log')
        self.test_logfilehandler = logging.FileHandler(
            self.test_logfile, mode='w')
        fileformatstring = ('%(asctime)s|%(process)d|%(threadName)s|%(name)s|'
                            '%(levelname)s|%(message)s')
        fileformatter = logging.Formatter(fileformatstring)
        self.test_logfilehandler.setFormatter(fileformatter)
        logger.addHandler(self.test_logfilehandler)

        self.loggerdeco = LogDecorator(logger,
                                       {'phoneid': self.phone.id,
                                        'buildid': self.build.id,
                                        'test': self.name},
                                       '%(phoneid)s|%(buildid)s|%(test)s|'
                                       '%(message)s')
        self.dm._logger = self.loggerdeco
        self.loggerdeco.debug('PhoneTest.setup_job')
        if self.unittest_logpath:
            os.unlink(self.unittest_logpath)
        self.unittest_logpath = None
        self.upload_dir = tempfile.mkdtemp()
        self.crash_processor = AutophoneCrashProcessor(self.dm,
                                                       self.profile_path,
                                                       self.upload_dir,
                                                       self.build.app_name)
        self.crash_processor.clear()
        self.test_result = PhoneTestResult()
        if not self.worker_subprocess.is_disabled():
            self.update_status(phone_status=PhoneStatus.WORKING,
                               message='Setting up %s' % self.name)

    def run_job(self):
        raise NotImplementedError

    def teardown_job(self):
        self.loggerdeco.debug('PhoneTest.teardown_job')
        self.stop_time = datetime.datetime.now()
        self.loggerdeco.info('Test %s elapsed time: %s' % (
            self.name, self.stop_time - self.start_time))
        try:
            if self.worker_subprocess.is_ok():
                # Do not attempt to process crashes if the device is
                # in an error state.
                self.handle_crashes()
        except Exception, e:
            self.loggerdeco.exception('Exception during crash processing')
            self.test_failure(
                self.name, 'TEST-UNEXPECTED-FAIL',
                'Exception %s during crash processing' % e,
                PhoneTestResult.EXCEPTION)
        logger = logging.getLogger('phonetest')
        if (logger.getEffectiveLevel() == logging.DEBUG and self.unittest_logpath and
            os.path.exists(self.unittest_logpath)):
            self.loggerdeco.debug(40 * '=')
            try:
                logfilehandle = open(self.unittest_logpath)
                self.loggerdeco.debug(logfilehandle.read())
                logfilehandle.close()
            except Exception:
                self.loggerdeco.exception('Exception %s loading log')
            self.loggerdeco.debug(40 * '-')
        # Log the current full contents of logcat, then reset the
        # logcat buffers to help prevent the device's buffer from
        # over flowing after the test.
        self.loggerdeco.debug('phonetest.teardown_job full logcat after job:')
        self.loggerdeco.debug('\n'.join(self.logcat.get(full=True)))
        try:
            if (self.worker_subprocess.is_disabled() and
                self.test_result.status != PhoneTestResult.USERCANCEL):
                # The worker was disabled while running one test of a job.
                # Record the cancellation on any remaining tests in that job.
                self.test_failure(self.name, 'TEST_UNEXPECTED_FAIL',
                                  'The worker was disabled.',
                                  PhoneTestResult.USERCANCEL)
            self.worker_subprocess.treeherder.submit_complete(
                self.phone.id,
                self.build.url,
                self.build.tree,
                self.build.revision_hash,
                tests=[self])
        except:
            self.loggerdeco.exception('Exception tearing down job')
        finally:
            if self.upload_dir and os.path.exists(self.upload_dir):
                shutil.rmtree(self.upload_dir)
            self.upload_dir = None

        # Reset the tests' volatile members in order to prevent them
        # from being reused after a test has completed.
        self.test_result = PhoneTestResult()
        self.message = None
        self.job_guid = None
        self.job_details = []
        self.submit_timestamp = None
        self.start_timestamp = None
        self.end_timestamp = None
        self.upload_dir = None
        self.start_time = None
        self.stop_time = None
        self.unittest_logpath = None
        self.logcat.reset()
        if self.loggerdeco_original:
            self.loggerdeco = self.loggerdeco_original
            self.loggerdeco_original = None
        if self.dm_logger_original:
            self.dm._logger = self.dm_logger_original
            self.dm_logger_original = None

        self.test_logfilehandler.close()
        logger.removeHandler(self.test_logfilehandler)
        self.test_logfilehandler = None
        os.unlink(self.test_logfile)
        self.test_logfile = None
Example #21
0
    def runtests(self, build_metadata, worker_subprocess):
        self.loggerdeco = LogDecorator(
            self.logger, {
                'phoneid': self.phone_cfg['phoneid'],
                'phoneip': self.phone_cfg['ip'],
                'buildid': build_metadata['buildid']
            }, '%(phoneid)s|%(phoneip)s|%(buildid)s|'
            '%(message)s')
        self.dm._logger = self.loggerdeco
        appname = build_metadata['androidprocname']

        if not self.install_local_pages():
            self.set_status(msg='Could not install local pages on phone. '
                            'Aborting test for '
                            'build %s' % build_metadata['buildid'])
            return

        if not self.create_profile(build_metadata):
            self.set_status(msg='Could not run Fennec. Aborting test for '
                            'build %s' % build_metadata['buildid'])
            return

        testcount = len(self._urls.keys())
        for testnum, (testname, url) in enumerate(self._urls.iteritems(), 1):
            self.loggerdeco = LogDecorator(
                self.logger, {
                    'phoneid': self.phone_cfg['phoneid'],
                    'phoneip': self.phone_cfg['ip'],
                    'buildid': build_metadata['buildid'],
                    'testname': testname
                }, '%(phoneid)s|%(phoneip)s|%(buildid)s|'
                '%(testname)s|%(message)s')
            self.dm._logger = self.loggerdeco
            if self.check_results(build_metadata, testname):
                # We already have good results for this test and build.
                # No need to test it again.
                self.loggerdeco.info(
                    'Skipping test (%d/%d) for %d iterations' %
                    (testnum, testcount, self._iterations))
                continue
            self.loggerdeco.info('Running test (%d/%d) for %d iterations' %
                                 (testnum, testcount, self._iterations))

            for attempt in range(self.stderrp_attempts):
                # dataset is a list of the measurements made for the
                # iterations for this test.
                #
                # An empty item in the dataset list represents a
                # failure to obtain any measurement for that
                # iteration.
                #
                # It is possible for an item in the dataset to have an
                # uncached value and not have a corresponding cached
                # value if the cached test failed to record the
                # values.

                dataset = []
                for iteration in range(self._iterations):
                    self.set_status(msg='Attempt %d/%d for Test %d/%d, '
                                    'run %d, for url %s' %
                                    (attempt + 1, self.stderrp_attempts,
                                     testnum, testcount, iteration + 1, url))

                    dataset.append({})

                    if not self.create_profile(build_metadata):
                        continue

                    measurement = self.runtest(build_metadata, appname, url)
                    if not measurement:
                        continue
                    dataset[-1]['uncached'] = measurement

                    measurement = self.runtest(build_metadata, appname, url)
                    if not measurement:
                        continue
                    dataset[-1]['cached'] = measurement

                    if self.is_stderr_below_threshold(dataset,
                                                      self.stderrp_accept):
                        self.loggerdeco.info(
                            'Accepted test (%d/%d) after %d of %d iterations' %
                            (testnum, testcount, iteration + 1,
                             self._iterations))
                        break

                self.loggerdeco.debug('publishing results')

                if self.is_stderr_below_threshold(dataset,
                                                  self.stderrp_reject):
                    rejected = False
                else:
                    rejected = True
                    self.loggerdeco.info(
                        'Rejected test (%d/%d) after %d/%d iterations' %
                        (testnum, testcount, iteration + 1, self._iterations))

                for datapoint in dataset:
                    for cachekey in datapoint:
                        self.publish_results(
                            starttime=datapoint[cachekey]['starttime'],
                            tstrt=datapoint[cachekey]['throbberstart'],
                            tstop=datapoint[cachekey]['throbberstop'],
                            build_metadata=build_metadata,
                            testname=testname,
                            cache_enabled=(cachekey == 'cached'),
                            rejected=rejected)
                if not rejected:
                    break
Example #22
0
class S1S2Test(PhoneTest):
    def runjob(self, build_metadata, worker_subprocess):
        logger = self.logger
        loggerdeco = self.loggerdeco
        self.logger = logging.getLogger('autophone.worker.subprocess.test')
        self.loggerdeco = LogDecorator(
            self.logger, {
                'phoneid': self.phone_cfg['phoneid'],
                'phoneip': self.phone_cfg['ip'],
                'buildid': build_metadata['buildid']
            }, '%(phoneid)s|%(phoneip)s|%(buildid)s|'
            '%(message)s')
        self.dm._logger = self.loggerdeco

        try:
            # Read our config file which gives us our number of
            # iterations and urls that we will be testing
            cfg = ConfigParser.RawConfigParser()
            cfg.read(self.config_file)
            # [signature]
            self._signer = None
            self._jwt = {'id': '', 'key': None}
            for opt in self._jwt.keys():
                try:
                    self._jwt[opt] = cfg.get('signature', opt)
                except (ConfigParser.NoSectionError,
                        ConfigParser.NoOptionError):
                    break
            # phonedash requires both an id and a key.
            if self._jwt['id'] and self._jwt['key']:
                self._signer = jwt.jws.HmacSha(key=self._jwt['key'],
                                               key_id=self._jwt['id'])
            # [paths]
            autophone_directory = os.path.dirname(os.path.abspath(sys.argv[0]))
            self._paths = {}
            self._paths['source'] = os.path.join(autophone_directory, 'files/')
            self._paths['dest'] = posixpath.join(self.base_device_path,
                                                 's1test/')
            self._paths['remote'] = 'http://phonedash.mozilla.org/'
            try:
                for opt in ('source', 'dest', 'profile', 'remote'):
                    try:
                        self._paths[opt] = cfg.get('paths', opt)
                        if not self._paths[opt].endswith('/'):
                            self._paths[opt] += '/'
                    except ConfigParser.NoOptionError:
                        pass
            except ConfigParser.NoSectionError:
                pass
            if 'profile' in self._paths:
                self.profile_path = self._paths['profile']
            # _pushes = {'sourcepath' : 'destpath', ...}
            self._pushes = {}
            for push in glob.glob(self._paths['source'] + '*'):
                if push.endswith('~') or push.endswith('.bak'):
                    continue
                push_dest = posixpath.join(self._paths['dest'],
                                           os.path.basename(push))
                self._pushes[push] = push_dest
            # [tests]
            self._tests = {}
            for t in cfg.items('tests'):
                self._tests[t[0]] = t[1]
            # Map URLS - {urlname: url} - urlname serves as testname
            self._urls = {}
            for test_location in ('local', 'remote'):
                for test_name in self._tests:
                    if test_location == 'local':
                        test_url = 'file://' + self._paths[
                            'dest'] + self._tests[test_name]
                    else:
                        test_url = self._paths['remote'] + self._tests[
                            test_name]
                    self._urls["%s-%s" % (test_location, test_name)] = test_url
            # [settings]
            self._iterations = cfg.getint('settings', 'iterations')
            try:
                self.stderrp_accept = cfg.getfloat('settings',
                                                   'stderrp_accept')
            except ConfigParser.NoOptionError:
                self.stderrp_accept = 0
            try:
                self.stderrp_reject = cfg.getfloat('settings',
                                                   'stderrp_reject')
            except ConfigParser.NoOptionError:
                self.stderrp_reject = 100
            try:
                self.stderrp_attempts = cfg.getint('settings',
                                                   'stderrp_attempts')
            except ConfigParser.NoOptionError:
                self.stderrp_attempts = 1
            self._resulturl = cfg.get('settings', 'resulturl')
            if not self._resulturl.endswith('/'):
                self._resulturl += '/'
            self._initialize_url = 'file://' + self._paths[
                'dest'] + 'initialize_profile.html'

            self.runtests(build_metadata, worker_subprocess)
        finally:
            self.logger = logger
            self.loggerdeco = loggerdeco
            self.dm._logger = loggerdeco

    def is_stderr_below_threshold(self, dataset, threshold):
        """Return True if all of the measurements in the dataset have
        standard errors of the mean below the threshold.

        Return False if at least one measurement is above the threshold
        or if one or more datasets have only one value.

        Return None if at least one measurement has no values.
        """

        self.loggerdeco.debug("is_stderr_below_threshold: %s" % dataset)

        for cachekey in ('uncached', 'cached'):
            for measurement in ('throbberstart', 'throbberstop'):
                data = [
                    datapoint[cachekey][measurement] -
                    datapoint[cachekey]['starttime'] for datapoint in dataset
                    if datapoint and cachekey in datapoint
                ]
                if not data:
                    return None
                stats = get_stats(data)
                self.loggerdeco.debug(
                    '%s %s count: %d, mean: %.2f, '
                    'stddev: %.2f, stderr: %.2f, '
                    'stderrp: %.2f' %
                    (cachekey, measurement, stats['count'], stats['mean'],
                     stats['stddev'], stats['stderr'], stats['stderrp']))
                if stats['count'] == 1 or stats['stderrp'] >= threshold:
                    return False
        return True

    def runtests(self, build_metadata, worker_subprocess):
        self.loggerdeco = LogDecorator(
            self.logger, {
                'phoneid': self.phone_cfg['phoneid'],
                'phoneip': self.phone_cfg['ip'],
                'buildid': build_metadata['buildid']
            }, '%(phoneid)s|%(phoneip)s|%(buildid)s|'
            '%(message)s')
        self.dm._logger = self.loggerdeco
        appname = build_metadata['androidprocname']

        if not self.install_local_pages():
            self.set_status(msg='Could not install local pages on phone. '
                            'Aborting test for '
                            'build %s' % build_metadata['buildid'])
            return

        if not self.create_profile(build_metadata):
            self.set_status(msg='Could not run Fennec. Aborting test for '
                            'build %s' % build_metadata['buildid'])
            return

        testcount = len(self._urls.keys())
        for testnum, (testname, url) in enumerate(self._urls.iteritems(), 1):
            self.loggerdeco = LogDecorator(
                self.logger, {
                    'phoneid': self.phone_cfg['phoneid'],
                    'phoneip': self.phone_cfg['ip'],
                    'buildid': build_metadata['buildid'],
                    'testname': testname
                }, '%(phoneid)s|%(phoneip)s|%(buildid)s|'
                '%(testname)s|%(message)s')
            self.dm._logger = self.loggerdeco
            if self.check_results(build_metadata, testname):
                # We already have good results for this test and build.
                # No need to test it again.
                self.loggerdeco.info(
                    'Skipping test (%d/%d) for %d iterations' %
                    (testnum, testcount, self._iterations))
                continue
            self.loggerdeco.info('Running test (%d/%d) for %d iterations' %
                                 (testnum, testcount, self._iterations))

            for attempt in range(self.stderrp_attempts):
                # dataset is a list of the measurements made for the
                # iterations for this test.
                #
                # An empty item in the dataset list represents a
                # failure to obtain any measurement for that
                # iteration.
                #
                # It is possible for an item in the dataset to have an
                # uncached value and not have a corresponding cached
                # value if the cached test failed to record the
                # values.

                dataset = []
                for iteration in range(self._iterations):
                    self.set_status(msg='Attempt %d/%d for Test %d/%d, '
                                    'run %d, for url %s' %
                                    (attempt + 1, self.stderrp_attempts,
                                     testnum, testcount, iteration + 1, url))

                    dataset.append({})

                    if not self.create_profile(build_metadata):
                        continue

                    measurement = self.runtest(build_metadata, appname, url)
                    if not measurement:
                        continue
                    dataset[-1]['uncached'] = measurement

                    measurement = self.runtest(build_metadata, appname, url)
                    if not measurement:
                        continue
                    dataset[-1]['cached'] = measurement

                    if self.is_stderr_below_threshold(dataset,
                                                      self.stderrp_accept):
                        self.loggerdeco.info(
                            'Accepted test (%d/%d) after %d of %d iterations' %
                            (testnum, testcount, iteration + 1,
                             self._iterations))
                        break

                self.loggerdeco.debug('publishing results')

                if self.is_stderr_below_threshold(dataset,
                                                  self.stderrp_reject):
                    rejected = False
                else:
                    rejected = True
                    self.loggerdeco.info(
                        'Rejected test (%d/%d) after %d/%d iterations' %
                        (testnum, testcount, iteration + 1, self._iterations))

                for datapoint in dataset:
                    for cachekey in datapoint:
                        self.publish_results(
                            starttime=datapoint[cachekey]['starttime'],
                            tstrt=datapoint[cachekey]['throbberstart'],
                            tstop=datapoint[cachekey]['throbberstop'],
                            build_metadata=build_metadata,
                            testname=testname,
                            cache_enabled=(cachekey == 'cached'),
                            rejected=rejected)
                if not rejected:
                    break

    def runtest(self, build_metadata, appname, url):
        # Clear logcat
        self.loggerdeco.debug('clearing logcat')
        self.dm.recordLogcat()
        self.loggerdeco.debug('logcat cleared')
        self.loggerdeco.debug('running fennec')

        # Run test
        self.run_fennec_with_profile(appname, url)

        # Get results - do this now so we don't have as much to
        # parse in logcat.
        starttime, throbberstart, throbberstop = self.analyze_logcat(
            build_metadata)

        self.wait_for_fennec(build_metadata)

        # Ensure we succeeded - no 0's reported
        datapoint = {}
        if (throbberstart and throbberstop):
            datapoint['starttime'] = starttime
            datapoint['throbberstart'] = throbberstart
            datapoint['throbberstop'] = throbberstop
            datapoint['throbbertime'] = throbberstop - throbberstart
        return datapoint

    def wait_for_fennec(self,
                        build_metadata,
                        max_wait_time=60,
                        wait_time=5,
                        kill_wait_time=20):
        # Wait for up to a max_wait_time seconds for fennec to close
        # itself in response to the quitter request. Check that fennec
        # is still running every wait_time seconds. If fennec doesn't
        # close on its own, attempt up to 3 times to kill fennec, waiting
        # kill_wait_time seconds between attempts.
        # Return True if fennec exits on its own, False if it needs to be killed.
        # Re-raise the last exception if fennec can not be killed.
        max_wait_attempts = max_wait_time / wait_time
        for wait_attempt in range(max_wait_attempts):
            if not self.dm.processExist(build_metadata['androidprocname']):
                return True
            sleep(wait_time)
        self.loggerdeco.debug('killing fennec')
        max_killattempts = 3
        for kill_attempt in range(max_killattempts):
            try:
                self.dm.killProcess(build_metadata['androidprocname'])
                break
            except DMError:
                self.loggerdeco.exception('Attempt %d to kill fennec failed' %
                                          kill_attempt)
                if kill_attempt == max_killattempts - 1:
                    raise
                sleep(kill_wait_time)
        return False

    def create_profile(self, build_metadata, custom_prefs=None):
        # Create, install and initialize the profile to be
        # used in the test.

        telemetry_prompt = 999
        if build_metadata['blddate'] < '2013-01-03':
            telemetry_prompt = 2
        prefs = {
            'browser.firstrun.show.localepicker': False,
            'browser.sessionstore.resume_from_crash': False,
            'dom.ipc.plugins.flash.subprocess.crashreporter.enabled': False,
            'browser.firstrun.show.uidiscovery': False,
            'shell.checkDefaultClient': False,
            'browser.warnOnQuit': False,
            'browser.EULA.override': True,
            'toolkit.telemetry.prompted': telemetry_prompt,
            'toolkit.telemetry.notifiedOptOut': telemetry_prompt,
            'datareporting.healthreport.service.enabled': False,
        }
        if isinstance(custom_prefs, dict):
            prefs = dict(prefs.items() + custom_prefs.items())
        profile = FirefoxProfile(preferences=prefs,
                                 addons='%s/xpi/quitter.xpi' % os.getcwd())
        if not self.install_profile(profile):
            return False

        appname = build_metadata['androidprocname']
        buildid = build_metadata['buildid']
        success = False
        for attempt in range(self.user_cfg[PHONE_RETRY_LIMIT]):
            self.loggerdeco.debug('Attempt %d Initializing profile' % attempt)
            self.run_fennec_with_profile(appname, self._initialize_url)
            if self.wait_for_fennec(build_metadata):
                success = True
                break
            sleep(self.user_cfg[PHONE_RETRY_WAIT])

        if not success:
            msg = 'Failure initializing profile for build %s' % buildid
            self.loggerdeco.error(msg)
            self.set_status(msg=msg)

        return success

    def install_local_pages(self):
        success = False
        for attempt in range(self.user_cfg[PHONE_RETRY_LIMIT]):
            self.loggerdeco.debug('Attempt %d Installing local pages' %
                                  attempt)
            try:
                self.dm.mkDir(self._paths['dest'])
                for push_source in self._pushes:
                    push_dest = self._pushes[push_source]
                    if os.path.isdir(push_source):
                        self.dm.pushDir(push_source, push_dest)
                    else:
                        self.dm.pushFile(push_source, push_dest)
                success = True
                break
            except DMError:
                self.loggerdeco.exception('Attempt %d Installing local pages' %
                                          attempt)
                sleep(self.user_cfg[PHONE_RETRY_WAIT])

        if not success:
            self.loggerdeco.error('Failure installing local pages')

        return success

    def is_fennec_running(self, appname):
        for attempt in range(self.user_cfg[PHONE_RETRY_LIMIT]):
            try:
                return self.dm.processExist(appname)
            except DMError:
                self.loggerdeco.exception('Attempt %d is fennec running' %
                                          attempt)
                if attempt == self.user_cfg[PHONE_RETRY_LIMIT] - 1:
                    raise
                sleep(self.user_cfg[PHONE_RETRY_WAIT])

    def get_logcat_throbbers(self):
        for attempt in range(self.user_cfg[PHONE_RETRY_LIMIT]):
            try:
                return [
                    x.strip() for x in self.dm.getLogcat(filterSpecs=[
                        'GeckoToolbarDisplayLayout:*', 'SUTAgentAndroid:I',
                        '*:S'
                    ])
                ]
            except DMError:
                self.loggerdeco.exception('Attempt %d get logcat throbbers' %
                                          attempt)
                if attempt == self.user_cfg[PHONE_RETRY_LIMIT] - 1:
                    raise
                sleep(self.user_cfg[PHONE_RETRY_WAIT])

    def analyze_logcat(self, build_metadata):
        self.loggerdeco.debug('analyzing logcat')

        app_name = build_metadata['androidprocname']
        logcat_prefix = '(\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3})'
        throbber_prefix = 'I/GeckoToolbarDisplayLayout.* zerdatime (\d+) - Throbber'
        re_base_time = re.compile('%s' % logcat_prefix)
        re_start_time = re.compile('%s I/SUTAgentAndroid.* '
                                   'exec am start\s+.*-n %s/.App '
                                   '-a android.intent.action.VIEW' %
                                   (logcat_prefix, app_name))
        re_throbber_start_time = re.compile('%s %s start' %
                                            (logcat_prefix, throbber_prefix))
        re_throbber_stop_time = re.compile('%s %s stop' %
                                           (logcat_prefix, throbber_prefix))

        self.loggerdeco.debug('analyze_logcat: re_base_time: %s' %
                              re_base_time.pattern)
        self.loggerdeco.debug('analyze_logcat: re_start_time: %s' %
                              re_start_time.pattern)
        self.loggerdeco.debug('analyze_logcat: re_throbber_start_time: %s' %
                              re_throbber_start_time.pattern)
        self.loggerdeco.debug('analyze_logcat: re_throbber_stop_time: %s' %
                              re_throbber_stop_time.pattern)

        base_time = 0
        start_time = 0
        throbber_start_time = 0
        throbber_stop_time = 0

        attempt = 0
        max_time = 90  # maximum time to wait for throbbers
        wait_time = 3  # time to wait between attempts
        max_attempts = max_time / wait_time

        while (attempt < max_attempts
               and (throbber_start_time == 0 or throbber_stop_time == 0)):
            buf = self.get_logcat_throbbers()
            for line in buf:
                self.loggerdeco.debug('analyze_logcat: %s' % line)
                match = re_base_time.match(line)
                if match and not base_time:
                    base_time = match.group(1)
                    self.loggerdeco.debug('analyze_logcat: base_time: %s' %
                                          base_time)
                # we want the first throbberstart and throbberstop.
                match = re_start_time.match(line)
                if match:
                    start_time = match.group(1)
                    self.loggerdeco.debug('analyze_logcat: start_time: %s' %
                                          start_time)
                    continue
                match = re_throbber_start_time.match(line)
                if match and not throbber_start_time:
                    throbber_start_time = match.group(1)
                    self.loggerdeco.debug(
                        'analyze_logcat: throbber_start_time: %s' %
                        throbber_start_time)
                    continue
                match = re_throbber_stop_time.match(line)
                if match and not throbber_stop_time:
                    throbber_stop_time = match.group(1)
                    self.loggerdeco.debug(
                        'analyze_logcat: throbber_stop_time: %s' %
                        throbber_stop_time)
                    continue
                if throbber_start_time and throbber_stop_time:
                    break
            if throbber_start_time == 0 or throbber_stop_time == 0:
                sleep(wait_time)
                attempt += 1
        if self.check_for_crashes():
            self.loggerdeco.info('fennec crashed')
            fennec_crashed = True
        else:
            fennec_crashed = False
        if throbber_start_time and throbber_stop_time == 0 and not fennec_crashed:
            self.loggerdeco.info('Unable to find Throbber stop')

        # The captured time from the logcat lines is in the format
        # MM-DD HH:MM:SS.mmm. It is possible for the year to change
        # between the different times, so we need to make adjustments
        # if necessary. First, we assume the year does not change and
        # parse the dates as if they are in the current year. If
        # the dates violate the natural order start_time,
        # throbber_start_time, throbber_stop_time, we can adjust the
        # year.

        if base_time and start_time and throbber_start_time and throbber_stop_time:
            parse = lambda y, t: datetime.datetime.strptime(
                '%4d-%s' % (y, t), '%Y-%m-%d %H:%M:%S.%f')
            year = datetime.datetime.now().year
            base_time = parse(year, base_time)
            start_time = parse(year, start_time)
            throbber_start_time = parse(year, throbber_start_time)
            throbber_stop_time = parse(year, throbber_stop_time)

            if base_time > start_time:
                base_time.replace(year=year - 1)
            elif start_time > throbber_start_time:
                base_time.replace(year=year - 1)
                start_time.replace(year=year - 1)
            elif throbber_start_time > throbber_stop_time:
                base_time.replace(year=year - 1)
                start_time.replace(year=year - 1)
                throbber_start_time.replace(year - 1)

            # Convert the times to milliseconds from the base time.
            convert = lambda t1, t2: round((t2 - t1).total_seconds() * 1000.0)

            start_time = convert(base_time, start_time)
            throbber_start_time = convert(base_time, throbber_start_time)
            throbber_stop_time = convert(base_time, throbber_stop_time)

            self.loggerdeco.debug(
                'analyze_logcat: base: %s, start: %s, '
                'throbber start: %s, throbber stop: %s, '
                'throbber time: %s ' %
                (base_time, start_time, throbber_start_time,
                 throbber_stop_time, throbber_stop_time - throbber_start_time))
        else:
            self.loggerdeco.warning(
                'analyze_logcat: failed to get throbbers '
                'start_time: %s, throbber start: %s, throbber stop: %s' %
                (start_time, throbber_start_time, throbber_stop_time))
            start_time = throbber_start_time = throbber_stop_time = 0

        return (start_time, throbber_start_time, throbber_stop_time)

    def publish_results(self,
                        starttime=0,
                        tstrt=0,
                        tstop=0,
                        build_metadata=None,
                        testname='',
                        cache_enabled=True,
                        rejected=False):
        msg = (
            'Cached: %s Start Time: %s Throbber Start: %s Throbber Stop: %s '
            'Total Throbber Time: %s Rejected: %s' %
            (cache_enabled, starttime, tstrt, tstop, tstop - tstrt, rejected))
        self.loggerdeco.info('RESULTS: %s' % msg)

        # Create JSON to send to webserver
        resultdata = {}
        resultdata['phoneid'] = self.phone_cfg['phoneid']
        resultdata['testname'] = testname
        resultdata['starttime'] = starttime
        resultdata['throbberstart'] = tstrt
        resultdata['throbberstop'] = tstop
        resultdata['blddate'] = build_metadata['blddate']
        resultdata['cached'] = cache_enabled
        resultdata['rejected'] = rejected

        resultdata['revision'] = build_metadata['revision']
        resultdata['productname'] = build_metadata['androidprocname']
        resultdata['productversion'] = build_metadata['version']
        resultdata['osver'] = self.phone_cfg['osver']
        resultdata['bldtype'] = build_metadata['bldtype']
        resultdata['machineid'] = self.phone_cfg['machinetype']

        result = {'data': resultdata}
        # Upload
        if self._signer:
            encoded_result = jwt.encode(result, signer=self._signer)
            content_type = 'application/jwt'
        else:
            encoded_result = json.dumps(result)
            content_type = 'application/json; charset=utf-8'
        req = urllib2.Request(self._resulturl + 'add/', encoded_result,
                              {'Content-Type': content_type})
        try:
            f = urllib2.urlopen(req)
        except urllib2.URLError, e:
            self.loggerdeco.error('Could not send results to server: %s' % e)
        else:
Example #23
0
class SmokeTest(PhoneTest):
    def runjob(self, build_metadata, worker_subprocess):
        logger = self.logger
        loggerdeco = self.loggerdeco
        self.logger = logging.getLogger('autophone.worker.subprocess.test')
        self.loggerdeco = LogDecorator(
            self.logger, {
                'phoneid': self.phone_cfg['phoneid'],
                'phoneip': self.phone_cfg['ip'],
                'buildid': build_metadata['buildid']
            }, '%(phoneid)s|%(phoneip)s|%(buildid)s|'
            '%(message)s')

        try:
            self.runtest(build_metadata, worker_subprocess)
        finally:
            self.logger = logger
            self.loggerdeco = loggerdeco

    def runtest(self, build_metadata, worker_subprocess):
        try:
            os.unlink('smoketest_pass')
        except OSError:
            pass
        try:
            os.unlink('smoketest_fail')
        except OSError:
            pass

        # Read our config file which gives us our number of
        # iterations and urls that we will be testing
        self.prepare_phone(build_metadata)

        appname = build_metadata['androidprocname']

        # Clear logcat
        self.dm.recordLogcat()

        # Run test
        self.loggerdeco.debug('running fennec')
        self.run_fennec_with_profile(appname, 'about:fennec')

        self.loggerdeco.debug('analyzing logcat...')
        fennec_launched = self.analyze_logcat(build_metadata)
        start = datetime.datetime.now()
        while (not fennec_launched and (datetime.datetime.now() - start <=
                                        datetime.timedelta(seconds=60))):
            sleep(3)
            fennec_launched = self.analyze_logcat(build_metadata)

        if fennec_launched:
            self.loggerdeco.info('fennec successfully launched')
            file('smoketest_pass', 'w')
        else:
            self.loggerdeco.error('failed to launch fennec')
            file('smoketest_fail', 'w')

        self.loggerdeco.debug('killing fennec')
        # Get rid of the browser and session store files
        self.dm.killProcess(build_metadata['androidprocname'])

        self.loggerdeco.debug('removing sessionstore files')
        self.remove_sessionstore_files()

    def prepare_phone(self, build_metadata):
        prefs = {
            'browser.firstrun.show.localepicker': False,
            'browser.sessionstore.resume_from_crash': False,
            'browser.firstrun.show.uidiscovery': False,
            'shell.checkDefaultClient': False,
            'browser.warnOnQuit': False,
            'browser.EULA.override': True,
            'toolkit.telemetry.prompted': 999,
            'toolkit.telemetry.notifiedOptOut': 999
        }
        profile = FirefoxProfile(preferences=prefs)
        self.install_profile(profile)

    def analyze_logcat(self, build_metadata):
        buf = self.dm.getLogcat()
        got_start = False
        got_end = False

        for line in buf:
            if not got_start and 'Start proc org.mozilla.fennec' in line:
                got_start = True
            if not got_end and 'Throbber stop' in line:
                got_end = True
        return got_start and got_end
Example #24
0
class SmokeTest(PhoneTest):

    def runjob(self, build_metadata, worker_subprocess):
        logger = self.logger
        loggerdeco = self.loggerdeco
        self.logger = logging.getLogger('autophone.worker.subprocess.test')
        self.loggerdeco = LogDecorator(self.logger,
                                       {'phoneid': self.phone_cfg['phoneid'],
                                        'phoneip': self.phone_cfg['ip'],
                                        'buildid': build_metadata['buildid']},
                                       '%(phoneid)s|%(phoneip)s|%(buildid)s|'
                                       '%(message)s')

        try:
            self.runtest(build_metadata, worker_subprocess)
        finally:
            self.logger = logger
            self.loggerdeco = loggerdeco

    def runtest(self, build_metadata, worker_subprocess):
        try:
            os.unlink('smoketest_pass')
        except OSError:
            pass
        try:
            os.unlink('smoketest_fail')
        except OSError:
            pass

        # Read our config file which gives us our number of
        # iterations and urls that we will be testing
        self.prepare_phone(build_metadata)

        appname = build_metadata['androidprocname']

        # Clear logcat
        self.dm.recordLogcat()

        # Run test
        self.loggerdeco.debug('running fennec')
        self.run_fennec_with_profile(appname, 'about:fennec')

        self.loggerdeco.debug('analyzing logcat...')
        fennec_launched = self.analyze_logcat(build_metadata)
        start = datetime.datetime.now()
        while (not fennec_launched and (datetime.datetime.now() - start
                                        <= datetime.timedelta(seconds=60))):
            sleep(3)
            fennec_launched = self.analyze_logcat(build_metadata)

        if fennec_launched:
            self.loggerdeco.info('fennec successfully launched')
            file('smoketest_pass', 'w')
        else:
            self.loggerdeco.error('failed to launch fennec')
            file('smoketest_fail', 'w')

        self.loggerdeco.debug('killing fennec')
        # Get rid of the browser and session store files
        self.dm.killProcess(build_metadata['androidprocname'])

        self.loggerdeco.debug('removing sessionstore files')
        self.remove_sessionstore_files()

    def prepare_phone(self, build_metadata):
        prefs = { 'browser.firstrun.show.localepicker': False,
                  'browser.sessionstore.resume_from_crash': False,
                  'browser.firstrun.show.uidiscovery': False,
                  'shell.checkDefaultClient': False,
                  'browser.warnOnQuit': False,
                  'browser.EULA.override': True,
                  'toolkit.telemetry.prompted': 999,
                  'toolkit.telemetry.notifiedOptOut': 999 }
        profile = FirefoxProfile(preferences=prefs)
        self.install_profile(profile)

    def analyze_logcat(self, build_metadata):
        buf = self.dm.getLogcat()
        got_start = False
        got_end = False

        for line in buf:
            if not got_start and 'Start proc org.mozilla.fennec' in line:
                got_start = True
            if not got_end and 'Throbber stop' in line:
                got_end = True
        return got_start and got_end
Example #25
0
class PhoneTest(object):
    def __init__(self, phone, options, config_file=None,
                 enable_unittests=False, test_devices_repos={},
                 chunk=1):
        self.config_file = config_file
        self.cfg = ConfigParser.RawConfigParser()
        self.cfg.read(self.config_file)
        self.enable_unittests = enable_unittests
        self.test_devices_repos = test_devices_repos
        self.chunk = chunk
        self.chunks = 1
        self.update_status_cb = None
        self.phone = phone
        self.worker_subprocess = None
        self.options = options
        self.logger = logging.getLogger('autophone.phonetest')
        self.loggerdeco = LogDecorator(self.logger,
                                       {'phoneid': self.phone.id,
                                        'pid': os.getpid()},
                                       '%(phoneid)s|%(pid)s|%(message)s')
        self.logger_original = None
        self.loggerdeco_original = None
        self.dm_logger_original = None
        self.loggerdeco.info('init autophone.phonetest')
        self._base_device_path = ''
        self.profile_path = '/data/local/tmp/profile'
        self._dm = None
        self._repos = None
        self._log = None
        # Treeherder related items.
        self._job_name = None
        self._job_symbol = None
        self._group_name = None
        self._group_symbol = None
        self.test_result = PhoneTestResult()
        self.message = None
        self.job_guid = None
        self.job_details = []
        self.submit_timestamp = None
        self.start_timestamp = None
        self.end_timestamp = None
        self.logcat = Logcat(self)
        self.loggerdeco.debug('PhoneTest: %s, cfg sections: %s' % (self.__dict__, self.cfg.sections()))
        if not self.cfg.sections():
            self.loggerdeco.warning('Test configuration not found. '
                                    'Will use defaults.')
        # upload_dir will hold ANR traces, tombstones and other files
        # pulled from the device.
        self.upload_dir = None
        # crash_processor is an instance of AutophoneCrashProcessor that
        # is used by non-unittests to process device errors and crashes.
        self.crash_processor = None

    def _check_device(self):
        for attempt in range(1, self.options.phone_retry_limit+1):
            output = self._dm.get_state()
            if output == 'device':
                break
            self.loggerdeco.warning(
                'PhoneTest:_check_device Attempt: %d: %s' %
                (attempt, output))
            time.sleep(self.options.phone_retry_wait)
        if output != 'device':
            raise ADBError('PhoneTest:_check_device: Failed')

    @property
    def name_suffix(self):
        return  '-%s' % self.chunk if self.chunk > 1 else ''

    @property
    def name(self):
        return 'autophone-%s%s' % (self.__class__.__name__, self.name_suffix)

    @property
    def dm(self):
        if not self._dm:
            self.loggerdeco.info('PhoneTest: Connecting to %s...' % self.phone.id)
            self._dm = ADBDevice(device=self.phone.serial,
                                 logger_name='autophone.phonetest.adb',
                                 device_ready_retry_wait=self.options.device_ready_retry_wait,
                                 device_ready_retry_attempts=self.options.device_ready_retry_attempts,
                                 verbose=self.options.verbose)
            # Override mozlog.logger
            self._dm._logger = self.loggerdeco
            self.loggerdeco.info('PhoneTest: Connected.')
        self._check_device()
        return self._dm

    @property
    def base_device_path(self):
        if self._base_device_path:
            return self._base_device_path
        success = False
        e = None
        for attempt in range(1, self.options.phone_retry_limit+1):
            self._base_device_path = self.dm.test_root + '/autophone'
            self.loggerdeco.debug('Attempt %d creating base device path %s' % (
                attempt, self._base_device_path))
            try:
                if not self.dm.is_dir(self._base_device_path):
                    self.dm.mkdir(self._base_device_path, parents=True)
                success = True
                break
            except ADBError:
                self.loggerdeco.exception('Attempt %d creating base device '
                                          'path %s' % (
                                              attempt, self._base_device_path))
                time.sleep(self.options.phone_retry_wait)

        if not success:
            raise e

        self.loggerdeco.debug('base_device_path is %s' % self._base_device_path)

        return self._base_device_path

    @property
    def job_name(self):
        if not self.options.treeherder_url:
            return None
        if not self._job_name:
            self._job_name = self.cfg.get('treeherder', 'job_name')
        return self._job_name

    @property
    def job_symbol(self):
        if not self.options.treeherder_url:
            return None
        if not self._job_symbol:
            self._job_symbol = self.cfg.get('treeherder', 'job_symbol')
            if self.chunks > 1:
                self._job_symbol = "%s%s" %(self._job_symbol, self.chunk)
        return self._job_symbol

    @property
    def group_name(self):
        if not self.options.treeherder_url:
            return None
        if not self._group_name:
            self._group_name = self.cfg.get('treeherder', 'group_name')
        return self._group_name

    @property
    def group_symbol(self):
        if not self.options.treeherder_url:
            return None
        if not self._group_symbol:
            self._group_symbol = self.cfg.get('treeherder', 'group_symbol')
        return self._group_symbol

    @property
    def build(self):
        return self.worker_subprocess.build

    @property
    def buildername(self):
        return "%s %s opt test %s-%s" % (
            self.phone.platform, self.build.tree, self.job_name, self.job_symbol)

    def handle_crashes(self):
        if not self.crash_processor:
            return

        for error in self.crash_processor.get_errors(self.build.symbols,
                                                     self.options.minidump_stackwalk,
                                                     clean=False):
            if error['reason'] == 'java-exception':
                self.test_result.add_failure(self.name,
                                             'PROCESS-CRASH',
                                             error['signature'])
            elif error['reason'] == 'PROFILE-ERROR':
                self.test_result.add_failure(self.name,
                                             error['reason'],
                                             error['signature'])
            elif error['reason'] == 'PROCESS-CRASH':
                self.loggerdeco.info("PROCESS-CRASH | %s | "
                                     "application crashed [%s]" % (self.name,
                                                                   error['signature']))
                self.loggerdeco.info(error['stackwalk_output'])
                self.loggerdeco.info(error['stackwalk_errors'])

                self.test_result.add_failure(self.name,
                                             error['reason'],
                                             'application crashed [%s]' % error['signature'])
            else:
                self.loggerdeco.warning('Unknown error reason: %s' % error['reason'])

    def setup_job(self):
        self.logger_original = self.logger
        self.loggerdeco_original = self.loggerdeco
        self.dm_logger_original = self.dm._logger
        # Clear the log file if we are submitting logs to Treeherder.
        if (self.worker_subprocess.options.treeherder_url and
            self.worker_subprocess.build.revision_hash and
            self.worker_subprocess.s3_bucket):
            self.worker_subprocess.initialize_log_filehandler()
        self.worker_subprocess.treeherder.submit_running(tests=[self])

        self.logger = logging.getLogger('autophone.worker.subprocess.test')
        self.loggerdeco = LogDecorator(self.logger,
                                       {'phoneid': self.phone.id,
                                        'pid': os.getpid(),
                                        'buildid': self.build.id,
                                        'test': self.name},
                                       '%(phoneid)s|%(pid)s|%(buildid)s|%(test)s|'
                                       '%(message)s')
        self.dm._logger = self.loggerdeco
        self.loggerdeco.debug('PhoneTest.setup_job')
        if self._log:
            os.unlink(self._log)
        self._log = None
        self.upload_dir = tempfile.mkdtemp()
        self.test_result = PhoneTestResult()

    def run_job(self):
        raise NotImplementedError

    def teardown_job(self):
        self.loggerdeco.debug('PhoneTest.teardown_job')
        try:
            self.handle_crashes()
            self.worker_subprocess.treeherder.submit_complete(test=self)
        finally:
            if self.upload_dir and os.path.exists(self.upload_dir):
                shutil.rmtree(self.upload_dir)
            self.upload_dir = None

        if self.logger.getEffectiveLevel() == logging.DEBUG and self._log:
            self.loggerdeco.debug(40 * '=')
            logfilehandle = open(self._log)
            self.loggerdeco.debug(logfilehandle.read())
            logfilehandle.close()
            self.loggerdeco.debug(40 * '-')
        self._log = None
        self.logcat = Logcat(self)
        if self.logger_original:
            self.logger = self.logger_original
        if self.loggerdeco_original:
            self.loggerdeco = self.loggerdeco_original
        if self.dm_logger_original:
            self.dm._logger = self.dm_logger_original

    def set_dm_debug(self, level):
        self.options.debug = level
        if self._dm:
            self._dm.log_level = level

    def update_status(self, phone_status=PhoneStatus.WORKING, message=None):
        if self.update_status_cb:
            self.update_status_cb(phone_status=phone_status,
                                  build=self.build,
                                  message=message)

    def install_profile(self, profile=None):
        if not profile:
            profile = FirefoxProfile()

        profile_path_parent = os.path.split(self.profile_path)[0]
        success = False
        for attempt in range(1, self.options.phone_retry_limit+1):
            try:
                self.loggerdeco.debug('Attempt %d installing profile' % attempt)
                if self.dm.is_dir(self.profile_path, root=True):
                    self.dm.rm(self.profile_path, recursive=True, root=True)
                self.dm.chmod(profile_path_parent, root=True)
                self.dm.mkdir(self.profile_path, root=True)
                self.dm.chmod(self.profile_path, root=True)
                self.dm.push(profile.profile, self.profile_path)
                self.dm.chmod(self.profile_path, recursive=True, root=True)
                success = True
                break
            except ADBError:
                self.loggerdeco.exception('Attempt %d Exception installing '
                                          'profile to %s' % (
                                              attempt, self.profile_path))
                time.sleep(self.options.phone_retry_wait)

        if not success:
            self.loggerdeco.error('Failure installing profile to %s' % self.profile_path)

        return success

    def run_fennec_with_profile(self, appname, url):
        self.loggerdeco.debug('run_fennec_with_profile: %s %s' % (appname, url))
        try:
            self.dm.pkill(appname, root=True)
            self.dm.launch_fennec(appname,
                                 intent="android.intent.action.VIEW",
                                 moz_env={'MOZ_CRASHREPORTER_NO_REPORT': '1',
                                          'MOZ_CRASHREPORTER': '1'},
                                 extra_args=['--profile', self.profile_path],
                                 url=url,
                                 wait=False,
                                 fail_if_running=False)
        except:
            self.loggerdeco.exception('run_fennec_with_profile: Exception:')
            raise

    def remove_sessionstore_files(self):
        self.dm.rm(self.profile_path + '/sessionstore.js', force=True)
        self.dm.rm(self.profile_path + '/sessionstore.bak', force=True)

    @property
    def fennec_crashed(self):
        """
        Perform a quick check for crashes by checking
        self.profile_path/minidumps for dump files.

        """
        if self.dm.exists(os.path.join(self.profile_path, 'minidumps', '*.dmp')):
            self.loggerdeco.info('fennec crashed')
            return True
        return False
Example #26
0
class PhoneTest(object):
    # Use instances keyed on phoneid+':'config_file+':'+str(chunk)
    # to lookup tests.

    instances = {}

    @classmethod
    def lookup(cls, phoneid, config_file, chunk):
        key = '%s:%s:%s' % (phoneid, config_file, chunk)
        if key in PhoneTest.instances:
            return PhoneTest.instances[key]
        return None

    @classmethod
    def match(cls, tests=None, test_name=None, phoneid=None,
              config_file=None, chunk=None, job_guid=None,
              build_url=None):

        logger.debug('PhoneTest.match(tests: %s, test_name: %s, phoneid: %s, '
                     'config_file: %s, chunk: %s, job_guid: %s, '
                     'build_url: %s' % (tests, test_name, phoneid,
                                        config_file, chunk, job_guid,
                                        build_url))
        matches = []
        if not tests:
            tests = [PhoneTest.instances[key] for key in PhoneTest.instances.keys()]

        for test in tests:
            if test_name and test_name != test.name:
                continue

            if phoneid and phoneid != test.phone.id:
                continue

            if config_file and config_file != test.config_file:
                continue

            if chunk and chunk != test.chunk:
                continue

            if job_guid and job_guid != test.job_guid:
                continue

            if build_url:
                abi = test.phone.abi
                sdk = test.phone.sdk
                # First assume the test and build are compatible.
                incompatible_job = False
                # x86 devices can only test x86 builds and non-x86
                # devices can not test x86 builds.
                if abi == 'x86':
                    if 'x86' not in build_url:
                        incompatible_job = True
                else:
                    if 'x86' in build_url:
                        incompatible_job = True
                # If the build_url does not contain an sdk level, then
                # assume this is an build from before the split sdk
                # builds were first created. Otherwise the build_url
                # must match this device's supported sdk levels.
                if ('api-9' not in build_url and 'api-10' not in build_url and
                    'api-11' not in build_url):
                    pass
                elif sdk not in build_url:
                    incompatible_job  = True

                if incompatible_job:
                    continue

                # The test may be defined for multiple repositories.
                # We are interested if this particular build is
                # supported by this test. First assume it is
                # incompatible, and only accept it if the build_url is
                # from one of the supported repositories.
                if test.repos:
                    incompatible_job = True
                    for repo in test.repos:
                        if repo in build_url:
                            incompatible_job = False
                            break
                    if incompatible_job:
                        continue

            matches.append(test)

        logger.debug('PhoneTest.match = %s' % matches)

        return matches

    def __init__(self, dm=None, phone=None, options=None, config_file=None, chunk=1, repos=[]):
        # Ensure that repos is a list and that it is sorted in order
        # for comparisons with the tests loaded from the jobs database
        # to work.
        assert type(repos) == list, 'PhoneTest repos argument must be a list'
        repos.sort()
        self._add_instance(phone.id, config_file, chunk)
        self.config_file = config_file
        self.cfg = ConfigParser.ConfigParser()
        self.cfg.read(self.config_file)
        self.enable_unittests = False
        self.chunk = chunk
        self.chunks = 1
        self.update_status_cb = None
        self.dm = dm
        self.phone = phone
        self.worker_subprocess = None
        self.options = options
        self.loggerdeco = LogDecorator(logger,
                                       {'phoneid': self.phone.id},
                                       '%(phoneid)s|%(message)s')
        self.loggerdeco_original = None
        self.dm_logger_original = None
        self.loggerdeco.info('init autophone.phonetest')
        self._base_device_path = ''
        self.profile_path = '/data/local/tmp/profile'
        self.repos = repos
        self._log = None
        # Treeherder related items.
        self._job_name = None
        self._job_symbol = None
        self._group_name = None
        self._group_symbol = None
        self.test_result = PhoneTestResult()
        self.message = None
        # A unique consistent guid is necessary for identifying the
        # test job in treeherder. The test job_guid is updated when a
        # test is added to the pending jobs/tests in the jobs
        # database.
        self.job_guid = None
        self.job_details = []
        self.submit_timestamp = None
        self.start_timestamp = None
        self.end_timestamp = None
        self.logcat = Logcat(self)
        self.loggerdeco.debug('PhoneTest: %s, cfg sections: %s' % (self.__dict__, self.cfg.sections()))
        if not self.cfg.sections():
            self.loggerdeco.warning('Test configuration not found. '
                                    'Will use defaults.')
        # upload_dir will hold ANR traces, tombstones and other files
        # pulled from the device.
        self.upload_dir = None
        # crash_processor is an instance of AutophoneCrashProcessor that
        # is used by non-unittests to process device errors and crashes.
        self.crash_processor = None
        # Instrument running time
        self.start_time = None
        self.stop_time = None
        self.loggerdeco.info('PhoneTest: Connected.')

    def __str__(self):
        return '%s(%s, config_file=%s, chunk=%s)' % (type(self).__name__,
                                                     self.phone,
                                                     self.config_file,
                                                     self.chunk)

    def __repr__(self):
        return self.__str__()

    def _add_instance(self, phoneid, config_file, chunk):
        key = '%s:%s:%s' % (phoneid, config_file, chunk)
        assert key not in PhoneTest.instances, 'Duplicate PhoneTest %s' % key
        PhoneTest.instances[key] = self

    def remove(self):
        key = '%s:%s:%s' % (self.phone.id, self.config_file, self.chunk)
        if key in PhoneTest.instances:
            del PhoneTest.instances[key]

    def _check_device(self):
        for attempt in range(1, self.options.phone_retry_limit+1):
            output = self.dm.get_state()
            if output == 'device':
                break
            self.loggerdeco.warning(
                'PhoneTest:_check_device Attempt: %d: %s' %
                (attempt, output))
            time.sleep(self.options.phone_retry_wait)
        if output != 'device':
            raise ADBError('PhoneTest:_check_device: Failed')

    @property
    def name_suffix(self):
        return  '-%s' % self.chunk if self.chunks > 1 else ''

    @property
    def name(self):
        return 'autophone-%s%s' % (self.__class__.__name__, self.name_suffix)

    @property
    def base_device_path(self):
        if self._base_device_path:
            return self._base_device_path
        success = False
        e = None
        for attempt in range(1, self.options.phone_retry_limit+1):
            self._base_device_path = self.dm.test_root + '/autophone'
            self.loggerdeco.debug('Attempt %d creating base device path %s' % (
                attempt, self._base_device_path))
            try:
                if not self.dm.is_dir(self._base_device_path):
                    self.dm.mkdir(self._base_device_path, parents=True)
                success = True
                break
            except ADBError:
                self.loggerdeco.exception('Attempt %d creating base device '
                                          'path %s' % (
                                              attempt, self._base_device_path))
                time.sleep(self.options.phone_retry_wait)

        if not success:
            raise e

        self.loggerdeco.debug('base_device_path is %s' % self._base_device_path)

        return self._base_device_path

    @property
    def job_name(self):
        if not self.options.treeherder_url:
            return None
        if not self._job_name:
            self._job_name = self.cfg.get('treeherder', 'job_name')
        return self._job_name

    @property
    def job_symbol(self):
        if not self.options.treeherder_url:
            return None
        if not self._job_symbol:
            self._job_symbol = self.cfg.get('treeherder', 'job_symbol')
            if self.chunks > 1:
                self._job_symbol = "%s%s" %(self._job_symbol, self.chunk)
        return self._job_symbol

    @property
    def group_name(self):
        if not self.options.treeherder_url:
            return None
        if not self._group_name:
            self._group_name = self.cfg.get('treeherder', 'group_name')
        return self._group_name

    @property
    def group_symbol(self):
        if not self.options.treeherder_url:
            return None
        if not self._group_symbol:
            self._group_symbol = self.cfg.get('treeherder', 'group_symbol')
        return self._group_symbol

    @property
    def build(self):
        return self.worker_subprocess.build

    def generate_guid(self):
        self.job_guid = utils.generate_guid()

    def get_buildername(self, tree):
        return "%s %s opt %s" % (
            self.phone.platform, tree, self.name)

    def handle_test_interrupt(self, reason):
        self.test_failure(self.name, 'TEST-UNEXPECTED-FAIL', reason,
                          PhoneTestResult.USERCANCEL)

    def test_pass(self, testpath):
        self.test_result.add_pass(testpath)

    def test_failure(self, testpath, status, message, testresult_status):
        self.message = message
        self.update_status(message=message)
        self.test_result.add_failure(testpath, status, message, testresult_status)

    def handle_crashes(self):
        if not self.crash_processor:
            return

        for error in self.crash_processor.get_errors(self.build.symbols,
                                                     self.options.minidump_stackwalk,
                                                     clean=False):
            if error['reason'] == 'java-exception':
                self.test_failure(
                    self.name, 'PROCESS-CRASH',
                    error['signature'],
                    PhoneTestResult.EXCEPTION)
            elif error['reason'] == 'PROFILE-ERROR':
                self.test_failure(
                    self.name,
                    error['reason'],
                    error['signature'],
                    PhoneTestResult.TESTFAILED)
            elif error['reason'] == 'PROCESS-CRASH':
                self.loggerdeco.info("PROCESS-CRASH | %s | "
                                     "application crashed [%s]" % (self.name,
                                                                   error['signature']))
                self.loggerdeco.info(error['stackwalk_output'])
                self.loggerdeco.info(error['stackwalk_errors'])

                self.test_failure(self.name,
                                  error['reason'],
                                  'application crashed [%s]' % error['signature'],
                                  PhoneTestResult.TESTFAILED)
            else:
                self.loggerdeco.warning('Unknown error reason: %s' % error['reason'])

    def setup_job(self):
        self.start_time = datetime.datetime.now()
        self.stop_time = self.start_time
        # Clear the Treeherder job details.
        self.job_details = []
        self.worker_subprocess.treeherder.submit_running(
            self.phone.id,
            self.build.url,
            self.build.tree,
            self.build.revision_hash,
            tests=[self])

        self.loggerdeco_original = self.loggerdeco
        # self.dm._logger can raise ADBTimeoutError due to the
        # property dm therefore place it after the initialization.
        self.dm_logger_original = self.dm._logger
        self.loggerdeco = LogDecorator(logger,
                                       {'phoneid': self.phone.id,
                                        'buildid': self.build.id,
                                        'test': self.name},
                                       '%(phoneid)s|%(buildid)s|%(test)s|'
                                       '%(message)s')
        self.dm._logger = self.loggerdeco
        self.loggerdeco.debug('PhoneTest.setup_job')
        if self._log:
            os.unlink(self._log)
        self._log = None
        self.upload_dir = tempfile.mkdtemp()
        self.test_result = PhoneTestResult()
        if not self.worker_subprocess.is_disabled():
            self.update_status(phone_status=PhoneStatus.WORKING,
                               message='Setting up %s' % self.name)

    def run_job(self):
        raise NotImplementedError

    def teardown_job(self):
        self.loggerdeco.debug('PhoneTest.teardown_job')
        self.stop_time = datetime.datetime.now()
        self.loggerdeco.info('Test %s elapsed time: %s' % (
            self.name, self.stop_time - self.start_time))
        try:
            self.handle_crashes()
        except Exception, e:
            self.test_failure(
                self.name, 'TEST-UNEXPECTED-FAIL',
                'Exception %s during crash processing' % e,
                PhoneTestResult.EXCEPTION)
        try:
            if (self.worker_subprocess.is_disabled() and
                self.test_result.status != PhoneTestResult.USERCANCEL):
                # The worker was disabled while running one test of a job.
                # Record the cancellation on any remaining tests in that job.
                self.test_failure(self.name, 'TEST_UNEXPECTED_FAIL',
                                  'The worker was disabled.',
                                  PhoneTestResult.USERCANCEL)
            self.worker_subprocess.treeherder.submit_complete(
                self.phone.id,
                self.build.url,
                self.build.tree,
                self.build.revision_hash,
                tests=[self])
        except:
            self.loggerdeco.exception('Exception tearing down job')
        finally:
            if self.upload_dir and os.path.exists(self.upload_dir):
                shutil.rmtree(self.upload_dir)
            self.upload_dir = None

        if (logger.getEffectiveLevel() == logging.DEBUG and self._log and
            os.path.exists(self._log)):
            self.loggerdeco.debug(40 * '=')
            try:
                logfilehandle = open(self._log)
                self.loggerdeco.debug(logfilehandle.read())
                logfilehandle.close()
            except Exception:
                self.loggerdeco.exception('Exception %s loading log')
            self.loggerdeco.debug(40 * '-')
        # Reset the tests' volatile members in order to prevent them
        # from being reused after a test has completed.
        self.test_result = PhoneTestResult()
        self.message = None
        self.job_guid = None
        self.job_details = []
        self.submit_timestamp = None
        self.start_timestamp = None
        self.end_timestamp = None
        self.upload_dir = None
        self.start_time = None
        self.stop_time = None
        self._log = None
        self.logcat = Logcat(self)
        if self.loggerdeco_original:
            self.loggerdeco = self.loggerdeco_original
        if self.dm_logger_original:
            self.dm._logger = self.dm_logger_original
Example #27
0
class PhoneWorkerSubProcess(object):
    """Worker subprocess.

    FIXME: Would be nice to have test results uploaded outside of the
    test objects, and to have them queued (and cached) if the results
    server is unavailable for some reason.  Might be best to communicate
    this back to the main AutoPhone process.
    """
    def __init__(self, worker_num, ipaddr, tests, phone_cfg, user_cfg,
                 autophone_queue, cmd_queue, logfile_prefix, loglevel, mailer,
                 build_cache_port):
        self.worker_num = worker_num
        self.ipaddr = ipaddr
        self.tests = tests
        self.phone_cfg = phone_cfg
        self.user_cfg = user_cfg
        self.autophone_queue = autophone_queue
        self.cmd_queue = cmd_queue
        self.logfile = logfile_prefix + '.log'
        self.outfile = logfile_prefix + '.out'
        self.loglevel = loglevel
        self.mailer = mailer
        self.build_cache_port = build_cache_port
        self._stop = False
        self.p = None
        self.jobs = jobs.Jobs(self.mailer, self.phone_cfg['phoneid'])
        self.current_build = None
        self.last_ping = None
        self._dm = None
        self.status = None
        self.logger = logging.getLogger('autophone.worker.subprocess')
        self.loggerdeco = LogDecorator(self.logger, {
            'phoneid': self.phone_cfg['phoneid'],
            'phoneip': self.phone_cfg['ip']
        }, '%(phoneid)s|%(phoneip)s|%(message)s')

    @property
    def dm(self):
        if not self._dm:
            self.loggerdeco.info(
                'Connecting to %s:%d...' %
                (self.phone_cfg['ip'], self.phone_cfg['sutcmdport']))
            # Droids and other slow phones can take a while to come back
            # after a SUT crash or spontaneous reboot, so we up the
            # default retrylimit.
            self._dm = DroidSUT(
                self.phone_cfg['ip'],
                self.phone_cfg['sutcmdport'],
                retryLimit=self.user_cfg[DEVICEMANAGER_RETRY_LIMIT],
                logLevel=self.loglevel)
            # Give slow devices chance to mount all devices.
            # Give slow devices chance to mount all devices.
            if self.user_cfg[DEVICEMANAGER_SETTLING_TIME] is not None:
                self._dm.reboot_settling_time = self.user_cfg[
                    DEVICEMANAGER_SETTLING_TIME]
            # Override mozlog.logger
            self._dm._logger = self.loggerdeco
            self.loggerdeco.info('Connected.')
        return self._dm

    def is_alive(self):
        """Call from main process."""
        return self.p and self.p.is_alive()

    def start(self, status):
        """Call from main process."""
        if self.p:
            if self.is_alive():
                return
            del self.p
        self.status = status
        self.p = multiprocessing.Process(target=self.run)
        self.p.start()

    def stop(self):
        """Call from main process."""
        if self.is_alive():
            self.cmd_queue.put_nowait(('stop', None))
            self.p.join(self.user_cfg[PHONE_COMMAND_QUEUE_TIMEOUT] * 2)

    def has_error(self):
        return (self.status == phonetest.PhoneTestMessage.DISABLED
                or self.status == phonetest.PhoneTestMessage.DISCONNECTED)

    def disconnect_dm(self):
        self._dm = None

    def status_update(self, msg):
        self.status = msg.status
        self.loggerdeco.info(str(msg))
        try:
            self.autophone_queue.put_nowait(msg)
        except Queue.Full:
            self.loggerdeco.warning('Autophone queue is full!')

    def check_sdcard(self):
        self.loggerdeco.info('Checking SD card.')
        success = True
        try:
            dev_root = self.dm.getDeviceRoot()
            if dev_root:
                d = posixpath.join(dev_root, 'autophonetest')
                self.dm.removeDir(d)
                self.dm.mkDir(d)
                if self.dm.dirExists(d):
                    with tempfile.NamedTemporaryFile() as tmp:
                        tmp.write('autophone test\n')
                        tmp.flush()
                        self.dm.pushFile(tmp.name,
                                         posixpath.join(d, 'sdcard_check'))
                    self.dm.removeDir(d)
                else:
                    self.loggerdeco.error('Failed to create directory under '
                                          'device root!')
                    success = False
            else:
                self.loggerdeco.error('Invalid device root.')
                success = False
        except DMError:
            self.loggerdeco.exception('Exception while checking SD card!')
            success = False

        if not success:
            # FIXME: Should this be called under more circumstances than just
            # checking the SD card?
            self.clear_test_base_paths()
            return False

        # reset status if there had previous been an error.
        # FIXME: should send email that phone is back up.
        self.status_update(
            phonetest.PhoneTestMessage(self.phone_cfg['phoneid'],
                                       phonetest.PhoneTestMessage.IDLE))
        return True

    def clear_test_base_paths(self):
        for t in self.tests:
            t._base_device_path = ''

    def recover_phone(self):
        exc = None
        reboots = 0
        while reboots < self.user_cfg[PHONE_MAX_REBOOTS]:
            self.loggerdeco.info('Rebooting phone...')
            reboots += 1
            # FIXME: reboot() no longer indicates success/failure; instead
            # we just verify the device root.
            try:
                self.dm.reboot(ipAddr=self.ipaddr, wait=True)
                if self.dm.getDeviceRoot():
                    self.loggerdeco.info('Phone is back up.')
                    if self.check_sdcard():
                        return
                    self.loggerdeco.info('Failed SD card check.')
                else:
                    self.loggerdeco.info('Phone did not reboot successfully.')
            except DMError:
                self.loggerdeco.exception('Exception while checking SD card!')
            # DM can be in a weird state if reboot failed.
            self.disconnect_dm()

        self.loggerdeco.info('Phone has been rebooted %d times; giving up.' %
                             reboots)
        msg_body = 'Phone was rebooted %d times.' % reboots
        if exc:
            msg_body += '\n\n%s' % exc
        self.phone_disconnected(msg_body)

    def reboot(self):
        self.status_update(
            phonetest.PhoneTestMessage(self.phone_cfg['phoneid'],
                                       phonetest.PhoneTestMessage.REBOOTING))
        self.recover_phone()

    def phone_disconnected(self, msg_body):
        """Indicate that a phone has become unreachable or experienced a
        error from which we might be able to recover."""
        if self.has_error():
            return
        self.loggerdeco.info('Phone disconnected: %s.' % msg_body)
        if msg_body and self.mailer:
            self.loggerdeco.info('Sending notification...')
            try:
                self.mailer.send(
                    'Phone %s disconnected' % self.phone_cfg['phoneid'],
                    '''Hello, this is Autophone. Phone %s appears to be disconnected:

%s

I'll keep trying to ping it periodically in case it reappears.
''' % (self.phone_cfg['phoneid'], msg_body))
                self.loggerdeco.info('Sent.')
            except socket.error:
                self.loggerdeco.exception('Failed to send disconnected-phone '
                                          'notification.')
        self.status_update(
            phonetest.PhoneTestMessage(
                self.phone_cfg['phoneid'],
                phonetest.PhoneTestMessage.DISCONNECTED))

    def disable_phone(self, errmsg, send_email=True):
        """Completely disable phone. No further attempts to recover it will
        be performed unless initiated by the user."""
        self.loggerdeco.info('Disabling phone: %s.' % errmsg)
        if errmsg and send_email and self.mailer:
            self.loggerdeco.info('Sending notification...')
            try:
                self.mailer.send(
                    'Phone %s disabled' % self.phone_cfg['phoneid'],
                    '''Hello, this is Autophone. Phone %s has been disabled:

%s

I gave up on it. Sorry about that. You can manually re-enable it with
the "enable" command.
''' % (self.phone_cfg['phoneid'], errmsg))
                self.loggerdeco.info('Sent.')
            except socket.error:
                self.loggerdeco.exception('Failed to send disabled-phone '
                                          'notification.')
        self.status_update(
            phonetest.PhoneTestMessage(self.phone_cfg['phoneid'],
                                       phonetest.PhoneTestMessage.DISABLED,
                                       msg=errmsg))

    def ping(self):
        self.loggerdeco.info('Pinging phone')
        # Verify that the phone is still responding.
        # It should always be possible to get the device root, so use this
        # command to ensure that the device is still reachable.
        try:
            if self.dm.getDeviceRoot():
                self.loggerdeco.info('Pong!')
                return True
        except DMError:
            self.loggerdeco.exception('Exception while pinging:')
        self.loggerdeco.error('Got empty device root!')
        return False

    def run_tests(self, build_metadata):
        if not self.has_error():
            self.loggerdeco.info('Rebooting...')
            self.reboot()

        # may have gotten an error trying to reboot, so test again
        if self.has_error():
            self.loggerdeco.info('Phone is in error state; not running test.')
            return False

        repo = build_metadata['tree']
        build_date = datetime.datetime.fromtimestamp(
            float(build_metadata['blddate']))

        self.status_update(
            phonetest.PhoneTestMessage(self.phone_cfg['phoneid'],
                                       phonetest.PhoneTestMessage.INSTALLING,
                                       build_metadata['blddate']))
        self.loggerdeco.info('Installing build %s.' % build_date)

        success = False
        for attempt in range(self.user_cfg[PHONE_RETRY_LIMIT]):
            try:
                pathOnDevice = posixpath.join(self.dm.getDeviceRoot(),
                                              'build.apk')
                self.dm.pushFile(
                    os.path.join(build_metadata['cache_build_dir'],
                                 'build.apk'), pathOnDevice)
                self.dm.installApp(pathOnDevice)
                self.dm.removeFile(pathOnDevice)
                success = True
            except DMError:
                exc = 'Exception installing fennec attempt %d!\n\n%s' % (
                    attempt, traceback.format_exc())
                self.loggerdeco.exception(
                    'Exception installing fennec attempt %d!' % attempt)
                time.sleep(self.user_cfg[PHONE_RETRY_WAIT])
        if not success:
            self.phone_disconnected(exc)
            return False
        self.current_build = build_metadata['blddate']

        self.loggerdeco.info('Running tests...')
        for t in self.tests:
            if self.has_error():
                break
            try:
                repos = t.test_devices_repos[self.phone_cfg['phoneid']]
                if repos and repo not in repos:
                    self.loggerdeco.debug('run_tests: ignoring build %s '
                                          'repo %s not in '
                                          'defined repos: %s' %
                                          (build_date, repo, repos))
                    continue
            except KeyError:
                pass

            t.current_build = build_metadata['blddate']
            try:
                t.runjob(build_metadata, self)
            except DMError:
                exc = 'Uncaught device error while running test!\n\n%s' % \
                    traceback.format_exc()
                self.loggerdeco.exception('Uncaught device error while '
                                          'running test!')
                self.phone_disconnected(exc)
                return False
        return True

    def handle_timeout(self):
        if (self.status != phonetest.PhoneTestMessage.DISABLED and
            (not self.last_ping or
             (datetime.datetime.now() - self.last_ping > datetime.timedelta(
                 seconds=self.user_cfg[PHONE_PING_INTERVAL])))):
            self.last_ping = datetime.datetime.now()
            if self.ping():
                if self.status == phonetest.PhoneTestMessage.DISCONNECTED:
                    self.recover_phone()
                if not self.has_error():
                    self.status_update(
                        phonetest.PhoneTestMessage(
                            self.phone_cfg['phoneid'],
                            phonetest.PhoneTestMessage.IDLE,
                            self.current_build))
            else:
                self.loggerdeco.info('Ping unanswered.')
                # No point in trying to recover, since we couldn't
                # even perform a simple action.
                if not self.has_error():
                    self.phone_disconnected('No response to ping.')

    def handle_job(self, job):
        phoneid = self.phone_cfg['phoneid']
        abi = self.phone_cfg['abi']
        build_url = job['build_url']
        self.loggerdeco.debug('handle_job: job: %s, abi: %s' % (job, abi))
        incompatible_job = False
        if abi == 'x86':
            if 'x86' not in build_url:
                incompatible_job = True
        elif abi == 'armeabi-v6':
            if 'armv6' not in build_url:
                incompatible_job = True
        else:
            if 'x86' in build_url or 'armv6' in build_url:
                incompatible_job = True
        if incompatible_job:
            self.loggerdeco.debug('Ignoring incompatible job %s '
                                  'for phone abi %s' % (build_url, abi))
            self.jobs.job_completed(job['id'])
            return
        # Determine if we will test this build and if we need
        # to enable unittests.
        skip_build = True
        enable_unittests = False
        for test in self.tests:
            test_devices_repos = test.test_devices_repos
            if not test_devices_repos:
                # We know we will test this build, but not yet
                # if any of the other tests enable_unittests.
                skip_build = False
            elif not phoneid in test_devices_repos:
                # This device will not run this test.
                pass
            else:
                for repo in test_devices_repos[phoneid]:
                    if repo in build_url:
                        skip_build = False
                        enable_unittests = test.enable_unittests
                        break
            if not skip_build:
                break
        if skip_build:
            self.loggerdeco.debug('Ignoring job %s ' % build_url)
            self.jobs.job_completed(job['id'])
            return
        self.loggerdeco.info('Checking job %s.' % build_url)
        client = buildserver.BuildCacheClient(port=self.build_cache_port)
        self.loggerdeco.info('Fetching build...')
        cache_response = client.get(build_url,
                                    enable_unittests=enable_unittests)
        client.close()
        if not cache_response['success']:
            self.loggerdeco.warning('Errors occured getting build %s: %s' %
                                    (build_url, cache_response['error']))
            return
        self.loggerdeco.info('Starting job %s.' % build_url)
        starttime = datetime.datetime.now()
        if self.run_tests(cache_response['metadata']):
            self.loggerdeco.info('Job completed.')
            self.jobs.job_completed(job['id'])
            self.status_update(
                phonetest.PhoneTestMessage(self.phone_cfg['phoneid'],
                                           phonetest.PhoneTestMessage.IDLE,
                                           self.current_build))
        else:
            self.loggerdeco.error('Job failed.')
        stoptime = datetime.datetime.now()
        self.loggerdeco.info('Job elapsed time: %s' % (stoptime - starttime))

    def handle_cmd(self, request):
        if not request:
            self.loggerdeco.debug('handle_cmd: No request')
            pass
        elif request[0] == 'stop':
            self.loggerdeco.info('Stopping at user\'s request...')
            self._stop = True
        elif request[0] == 'job':
            # This is just a notification that breaks us from waiting on the
            # command queue; it's not essential, since jobs are stored in
            # a db, but it allows the worker to react quickly to a request if
            # it isn't doing anything else.
            self.loggerdeco.debug('Received job command request...')
            pass
        elif request[0] == 'reboot':
            self.loggerdeco.info('Rebooting at user\'s request...')
            self.reboot()
        elif request[0] == 'disable':
            self.disable_phone('Disabled at user\'s request', False)
        elif request[0] == 'enable':
            self.loggerdeco.info('Enabling phone at user\'s request...')
            if self.has_error():
                self.status_update(
                    phonetest.PhoneTestMessage(self.phone_cfg['phoneid'],
                                               phonetest.PhoneTestMessage.IDLE,
                                               self.current_build))
                self.last_ping = None
        elif request[0] == 'debug':
            self.loggerdeco.info(
                'Setting debug level %d at user\'s request...' % request[1])
            self.user_cfg['debug'] = request[1]
            DroidSUT.debug = self.user_cfg['debug']
            # update any existing DroidSUT objects
            if self._dm:
                self._dm.loglevel = self.user_cfg['debug']
            for t in self.tests:
                t.set_dm_debug(self.user_cfg['debug'])
        elif request[0] == 'ping':
            self.loggerdeco.info('Pinging at user\'s request...')
            self.ping()
        else:
            self.loggerdeco.debug('handle_cmd: Unknown request %s' %
                                  request[0])

    def main_loop(self):
        # Commands take higher priority than jobs, so we deal with all
        # immediately available commands, then start the next job, if there is
        # one.  If neither a job nor a command is currently available,
        # block on the command queue for PhoneWorker.PHONE_COMMAND_QUEUE_TIMEOUT seconds.
        request = None
        while True:
            try:
                if not request:
                    request = self.cmd_queue.get_nowait()
                self.handle_cmd(request)
                request = None
                if self._stop:
                    return
            except Queue.Empty:
                request = None
                if self.has_error():
                    self.recover_phone()
                if not self.has_error():
                    job = self.jobs.get_next_job()
                    if job:
                        self.handle_job(job)
                    else:
                        try:
                            request = self.cmd_queue.get(
                                timeout=self.
                                user_cfg[PHONE_COMMAND_QUEUE_TIMEOUT])
                        except Queue.Empty:
                            request = None
                            self.handle_timeout()

    def run(self):
        sys.stdout = file(self.outfile, 'a', 0)
        sys.stderr = sys.stdout
        self.filehandler = MultiprocessingTimedRotatingFileHandler(
            self.logfile, when='midnight', backupCount=7)
        fileformatstring = ('%(asctime)s|%(levelname)s' '|%(message)s')
        self.fileformatter = logging.Formatter(fileformatstring)
        self.filehandler.setFormatter(self.fileformatter)
        self.logger.addHandler(self.filehandler)

        self.loggerdeco.info('PhoneWorker starting up.')

        DroidSUT.loglevel = self.user_cfg.get('debug', 3)

        for t in self.tests:
            t.status_cb = self.status_update

        self.status_update(
            phonetest.PhoneTestMessage(self.phone_cfg['phoneid'], self.status))

        if self.status != phonetest.PhoneTestMessage.DISABLED:
            if not self.check_sdcard():
                self.recover_phone()
            if self.has_error():
                self.loggerdeco.error('Initial SD card check failed.')

        self.main_loop()
Example #28
0
    def runtest(self, test_parameters):

        self.loggerdeco = LogDecorator(
            self.logger, {
                'phoneid': self.phone_cfg['phoneid'],
                'phoneip': self.phone_cfg['ip'],
                'buildid': test_parameters['buildid'],
                'testname': test_parameters['test_name']
            }, '%(phoneid)s|%(phoneip)s|%(buildid)s|'
            '%(testname)s|%(message)s')

        if self.logger.getEffectiveLevel() == logging.DEBUG:
            self.loggerdeco.debug('runtestsremote.py runtest start')
            for key in test_parameters.keys():
                self.loggerdeco.debug('test parameters: %s = %s' %
                                      (key, test_parameters[key]))

        self.set_status(msg='Starting test %s' % test_parameters['test_name'])

        test_parameters['harness_type'] = test_parameters['test_name']

        if test_parameters['test_name'] == 'robocoptest':
            test_parameters['harness_type'] = 'mochitest'
            robocop_apk_path = posixpath.join(self.dm.getDeviceRoot(),
                                              'robocop.apk')

            # XXX: FIXME. When bug 792072 lands, change to have
            # installApp() push the file

            self.dm.pushFile(
                os.path.join(test_parameters['cache_build_dir'],
                             'robocop.apk'), robocop_apk_path)
            try:
                self.dm.uninstallApp('org.mozilla.roboexample.test')
            except DMError:
                self.loggerdeco.exception(
                    'runtestsremote.py:runtest: Exception running test.')

            self.dm.installApp(robocop_apk_path)
            self.dm.removeFile(robocop_apk_path)

        test_parameters['port_manager'] = PortManager(
            test_parameters['host_ip_address'])

        for this_chunk in range(1, test_parameters['total_chunks'] + 1):

            test_parameters['this_chunk'] = this_chunk

            try:
                while True:
                    socket_collision = False

                    logfilehandle = tempfile.NamedTemporaryFile(delete=False)
                    self.loggerdeco.debug('logging to %s' % logfilehandle.name)

                    args = self.create_test_args(test_parameters)

                    test_parameters['cmdline'] = ' '.join(args)
                    self.loggerdeco.debug("cmdline = %s" %
                                          test_parameters['cmdline'])

                    self.set_status(msg='Running test %s chunk %d of %d' %
                                    (test_parameters['test_name'], this_chunk,
                                     test_parameters['total_chunks']))
                    if self.dm.processExist(
                            test_parameters['androidprocname']):
                        max_kill_attempts = 3
                        for kill_attempt in range(max_kill_attempts):
                            self.loggerdeco.debug(
                                'Process %s exists. Attempt %d to kill.' %
                                (test_parameters['androidprocname'],
                                 kill_attempt + 1))
                            self.dm.killProcess(
                                test_parameters['androidprocname'])
                            if not self.dm.processExist(
                                    test_parameters['androidprocname']):
                                break
                        if kill_attempt == max_kill_attempts - 1 and \
                                self.dm.processExist(test_parameters['androidprocname']):
                            self.loggerdeco.warning(
                                'Could not kill process %s.' %
                                (test_parameters['androidprocname']))
                    proc = subprocess.Popen(
                        args,
                        cwd=os.path.join(test_parameters['cache_build_dir'],
                                         'tests'),
                        preexec_fn=lambda: os.setpgid(0, 0),
                        stdout=logfilehandle,
                        stderr=subprocess.STDOUT,
                        close_fds=True)
                    proc.wait()
                    self.loggerdeco.debug('runtestsremote.py return code %d' %
                                          proc.returncode)

                    logfilehandle.close()
                    reSocketError = re.compile('socket\.error:')
                    logfilehandle = open(logfilehandle.name)
                    for logline in logfilehandle:
                        if reSocketError.search(logline):
                            socket_collision = True
                            break
                    logfilehandle.close()

                    if not socket_collision:
                        break

                self.set_status(msg='Completed test %s chunk %d of %d' %
                                (test_parameters['test_name'], this_chunk,
                                 test_parameters['total_chunks']))
            except:
                error_message = (
                    'Exception during test %s chunk %d of %d: %s' %
                    (test_parameters['test_name'], this_chunk,
                     test_parameters['total_chunks'], traceback.format_exc()))
                self.set_status(msg=error_message)
                self.loggerdeco.error(error_message)
            finally:
                logfilehandle.close()
                self.process_test_log(test_parameters, logfilehandle)
                if self.logger.getEffectiveLevel() == logging.DEBUG:
                    logfilehandle = open(logfilehandle.name)
                    self.loggerdeco.debug(40 * '*')
                    self.loggerdeco.debug(logfilehandle.read())
                    self.loggerdeco.debug(40 * '-')
                    logfilehandle.close()
                os.unlink(logfilehandle.name)
                # wait for a minute to give the phone time to settle
                time.sleep(60)
                # Recover the phone in between tests/chunks.
                self.loggerdeco.info('Rebooting device after test.')
                self.worker_subprocess.recover_phone()

        self.loggerdeco.debug('runtestsremote.py runtest exit')

        return
Example #29
0
    def runjob(self, build_metadata, worker_subprocess):
        logger = self.logger
        loggerdeco = self.loggerdeco
        self.logger = logging.getLogger('autophone.worker.subprocess.test')
        self.loggerdeco = LogDecorator(
            self.logger, {
                'phoneid': self.phone_cfg['phoneid'],
                'phoneip': self.phone_cfg['ip'],
                'buildid': build_metadata['buildid']
            }, '%(phoneid)s|%(phoneip)s|%(buildid)s|'
            '%(message)s')
        self.dm._logger = self.loggerdeco

        try:
            # Read our config file which gives us our number of
            # iterations and urls that we will be testing
            cfg = ConfigParser.RawConfigParser()
            cfg.read(self.config_file)
            # [signature]
            self._signer = None
            self._jwt = {'id': '', 'key': None}
            for opt in self._jwt.keys():
                try:
                    self._jwt[opt] = cfg.get('signature', opt)
                except (ConfigParser.NoSectionError,
                        ConfigParser.NoOptionError):
                    break
            # phonedash requires both an id and a key.
            if self._jwt['id'] and self._jwt['key']:
                self._signer = jwt.jws.HmacSha(key=self._jwt['key'],
                                               key_id=self._jwt['id'])
            # [paths]
            autophone_directory = os.path.dirname(os.path.abspath(sys.argv[0]))
            self._paths = {}
            self._paths['source'] = os.path.join(autophone_directory, 'files/')
            self._paths['dest'] = posixpath.join(self.base_device_path,
                                                 's1test/')
            self._paths['remote'] = 'http://phonedash.mozilla.org/'
            try:
                for opt in ('source', 'dest', 'profile', 'remote'):
                    try:
                        self._paths[opt] = cfg.get('paths', opt)
                        if not self._paths[opt].endswith('/'):
                            self._paths[opt] += '/'
                    except ConfigParser.NoOptionError:
                        pass
            except ConfigParser.NoSectionError:
                pass
            if 'profile' in self._paths:
                self.profile_path = self._paths['profile']
            # _pushes = {'sourcepath' : 'destpath', ...}
            self._pushes = {}
            for push in glob.glob(self._paths['source'] + '*'):
                if push.endswith('~') or push.endswith('.bak'):
                    continue
                push_dest = posixpath.join(self._paths['dest'],
                                           os.path.basename(push))
                self._pushes[push] = push_dest
            # [tests]
            self._tests = {}
            for t in cfg.items('tests'):
                self._tests[t[0]] = t[1]
            # Map URLS - {urlname: url} - urlname serves as testname
            self._urls = {}
            for test_location in ('local', 'remote'):
                for test_name in self._tests:
                    if test_location == 'local':
                        test_url = 'file://' + self._paths[
                            'dest'] + self._tests[test_name]
                    else:
                        test_url = self._paths['remote'] + self._tests[
                            test_name]
                    self._urls["%s-%s" % (test_location, test_name)] = test_url
            # [settings]
            self._iterations = cfg.getint('settings', 'iterations')
            try:
                self.stderrp_accept = cfg.getfloat('settings',
                                                   'stderrp_accept')
            except ConfigParser.NoOptionError:
                self.stderrp_accept = 0
            try:
                self.stderrp_reject = cfg.getfloat('settings',
                                                   'stderrp_reject')
            except ConfigParser.NoOptionError:
                self.stderrp_reject = 100
            try:
                self.stderrp_attempts = cfg.getint('settings',
                                                   'stderrp_attempts')
            except ConfigParser.NoOptionError:
                self.stderrp_attempts = 1
            self._resulturl = cfg.get('settings', 'resulturl')
            if not self._resulturl.endswith('/'):
                self._resulturl += '/'
            self._initialize_url = 'file://' + self._paths[
                'dest'] + 'initialize_profile.html'

            self.runtests(build_metadata, worker_subprocess)
        finally:
            self.logger = logger
            self.loggerdeco = loggerdeco
            self.dm._logger = loggerdeco
Example #30
0
    def __init__(self, dm=None, phone=None, options=None, config_file=None, chunk=1, repos=[]):
        # The PhoneTest constructor may raise exceptions due to issues with
        # the device. Creators of PhoneTest objects are responsible
        # for catching these exceptions and cleaning up any previously
        # created tests for the device.
        #
        # Ensure that repos is a list and that it is sorted in order
        # for comparisons with the tests loaded from the jobs database
        # to work.
        assert type(repos) == list, 'PhoneTest repos argument must be a list'
        repos.sort()
        self._add_instance(phone.id, config_file, chunk)
        # The default preferences and environment for running fennec
        # are set here in PhoneTest. Tests which subclass PhoneTest can
        # add or override these preferences during their
        # initialization.
        self._preferences = None
        self._environment = None
        self.config_file = config_file
        self.cfg = ConfigParser.ConfigParser()
        # Make the values in the config file case-sensitive
        self.cfg.optionxform = str
        self.cfg.read(self.config_file)
        self.enable_unittests = False
        self.chunk = chunk
        self.chunks = 1
        self.update_status_cb = None
        self.dm = dm
        self.phone = phone
        self.worker_subprocess = None
        self.options = options

        logger = utils.getLogger(name=self.phone.id)
        self.loggerdeco = LogDecorator(logger,
                                       {},
                                       '%(message)s')
        self.loggerdeco_original = None
        self.dm_logger_original = None
        # Test result
        self.status = TreeherderStatus.SUCCESS
        self.passed = 0
        self.failed = 0
        self.todo = 0

        self.repos = repos
        self.unittest_logpath = None
        # Treeherder related items.
        self._job_name = None
        self._job_symbol = None
        self._group_name = None
        self._group_symbol = None
        self.message = None
        # A unique consistent guid is necessary for identifying the
        # test job in treeherder. The test job_guid is updated when a
        # test is added to the pending jobs/tests in the jobs
        # database.
        self.job_guid = None
        self.job_details = []
        self.submit_timestamp = None
        self.start_timestamp = None
        self.end_timestamp = None
        if not self.cfg.sections():
            self.loggerdeco.warning('Test configuration not found. '
                                    'Will use defaults.')
        # upload_dir will hold ANR traces, tombstones and other files
        # pulled from the device.
        self.upload_dir = None
        # crash_processor is an instance of AutophoneCrashProcessor that
        # is used by non-unittests to process device errors and crashes.
        self.crash_processor = None
        # Instrument running time
        self.start_time = None
        self.stop_time = None
        # Perform initial configuration. For tests which do not
        # specify all config options, reasonable defaults will be
        # chosen.

        # [paths]

        # base_device_path accesses the device to determine the
        # appropriate path and can therefore fail and raise an
        # exception which will not be caught in the PhoneTest
        # constructor. Creators of PhoneTest objects are responsible
        # for catching these exceptions and cleaning up any previously
        # created tests for the device.
        self._base_device_path = ''
        self.profile_path = '/data/local/tests/autophone/profile'
        if self.dm:
            self.profile_path = '%s/profile' % self.base_device_path
        self.autophone_directory = os.path.dirname(os.path.abspath(sys.argv[0]))
        self._paths = {}
        try:
            sources = self.cfg.get('paths', 'sources').split()
            self._paths['sources'] = []
            for source in sources:
                if not source.endswith('/'):
                    source += '/'
                self._paths['sources'].append(source)
        except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
            self._paths['sources'] = ['files/base/']
        try:
            self._paths['dest'] = self.cfg.get('paths', 'dest')
            if not self._paths['dest'].endswith('/'):
                self._paths['dest'] += '/'
        except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
            self._paths['dest'] = os.path.join(self.base_device_path,
                                               self.__class__.__name__)
        try:
            self._paths['profile'] = self.cfg.get('paths', 'profile')
            if not self._paths['profile'].endswith('/'):
                self._paths['profile'] += '/'
        except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
            pass
        self.run_if_changed = set()
        try:
            dirs = self.cfg.get('runtests', 'run_if_changed')
            self.run_if_changed = set([d.strip() for d in dirs.split(',')])
            if self.run_if_changed:
                PhoneTest.has_run_if_changed = True
        except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
            pass
        if 'profile' in self._paths:
            self.profile_path = self._paths['profile']
        # _pushes = {'sourcepath' : 'destpath', ...}
        self._pushes = {}
        for source in self._paths['sources']:
            for push in glob.glob(source + '*'):
                if push.endswith('~') or push.endswith('.bak'):
                    continue
                push_file_name = os.path.basename(push)
                push_dest = posixpath.join(self._paths['dest'],
                                           source,
                                           push_file_name)
                self._pushes[push] = push_dest
                if push_file_name == 'initialize_profile.html':
                    self._initialize_url = 'file://' + push_dest
        # [tests]
        self._tests = {}
        try:
            for t in self.cfg.items('tests'):
                self._tests[t[0]] = t[1]
        except ConfigParser.NoSectionError:
            self._tests['blank'] = 'blank.html'

        # [builds]
        self.buildtypes = []
        try:
            self.buildtypes = self.cfg.get('builds', 'buildtypes').split(' ')
        except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
            self.buildtypes = list(self.options.buildtypes)

        self.platforms = []
        try:
            self.platforms = self.cfg.get('builds', 'platforms').split(' ')
        except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
            self.platforms = self.options.platforms

        self.loggerdeco.info('PhoneTest: %s', self.__dict__)
Example #31
0
class UnitTest(PhoneTest):

    def runjob(self, build_metadata, worker_subprocess):
        logger = self.logger
        loggerdeco = self.loggerdeco
        self.logger = logging.getLogger('autophone.worker.subprocess.test')
        self.loggerdeco = LogDecorator(self.logger,
                                       {'phoneid': self.phone_cfg['phoneid'],
                                        'phoneip': self.phone_cfg['ip'],
                                        'buildid': build_metadata['buildid']},
                                       '%(phoneid)s|%(phoneip)s|%(buildid)s|'
                                       '%(message)s')
        try:
            self.setup_and_runtest(build_metadata, worker_subprocess)
        finally:
            self.logger = logger
            self.loggerdeco = loggerdeco

    def setup_and_runtest(self, build_metadata, worker_subprocess):
        self.loggerdeco.debug('runtestsremote.py runjob start')
        self.set_status(msg='runtestsremote.py runjob start')

        self.worker_subprocess = worker_subprocess
        self.worker_subprocess.check_sdcard()

        host_ip_address = self.phone_cfg['ipaddr']
        phone_ip_address = self.phone_cfg['ip']
        device_port = self.phone_cfg['sutcmdport']

        cache_build_dir = os.path.abspath(build_metadata["cache_build_dir"])
        symbols_path = os.path.join(cache_build_dir, 'symbols')
        if not os.path.exists(symbols_path):
            symbols_path = None

        androidprocname = build_metadata['androidprocname']
        re_revision = re.compile(r'http.*/rev/(.*)')
        match = re_revision.match(build_metadata['revision'])
        if match:
            revision = match.group(1)
        else:
            revision = build_metadata['revision']
        buildid = build_metadata['buildid']
        tree = build_metadata['tree']

        if self.logger.getEffectiveLevel() == logging.DEBUG:
            for prop in self.phone_cfg:
                self.loggerdeco.debug('phone_cfg[%s] = %s' %
                                      (prop, self.phone_cfg[prop]))

        job_cfg = ConfigParser.RawConfigParser()
        job_cfg.read(self.config_file)

        if job_cfg.has_option('runtests', 'config_files'):
            # job contains multiple unittests
            config_files = job_cfg.get('runtests', 'config_files').split(' ')
        elif job_cfg.has_option('runtests', 'test_name'):
            # job contains a single unittest
            config_files = [self.config_file]
        else:
            raise Exception('Job configuration %s does not specify a test' %
                            self.config_file)
        missing_config_files = []
        for config_file in config_files:
            if not os.path.exists(config_file):
                missing_config_files.append(config_file)
        if missing_config_files:
            raise Exception("Can not run tests with missing config files: %s" %
                            ', '.join(missing_config_files))
        for config_file in config_files:
            try:
                test_parameters = {
                    'host_ip_address': host_ip_address,
                    'phone_ip_address': phone_ip_address,
                    'device_port': device_port,
                    'androidprocname': androidprocname,
                    'cache_build_dir': cache_build_dir,
                    'symbols_path': symbols_path,
                    'revision': revision,
                    'buildid': buildid,
                    'tree': tree,
                    'config_file': config_file,
                }

                self.load_test_parameters(test_parameters, config_file)
                self.runtest(test_parameters)

            except:
                # This exception handler deals with exceptions which occur outside
                # of the actual test runner. Exceptions from the test runner
                # are handled locally in runtest.
                self.loggerdeco.exception('runtestsremote.py:runjob: Exception '
                                          'running test')
                self.set_status(msg='runtestsremote.py:runjob: Exception '
                                'running test')
                # give the phone a minute to recover
                time.sleep(60)
                self.worker_subprocess.recover_phone()

        self.loggerdeco.debug('runtestsremote.py runjob exit')
        self.set_status(msg='runtestsremote.py runjob exit')

    def load_test_parameters(self, test_parameters, config_file):

        cfg = ConfigParser.RawConfigParser()
        cfg.read(config_file)

        test_parameters['test_name'] = cfg.get('runtests', 'test_name')
        test_parameters['test_manifest'] = cfg.get('runtests', 'test_manifest')
        try:
            test_parameters['test_path'] = cfg.get('runtests', 'test_path')
        except ConfigParser.NoOptionError:
            test_parameters['test_path'] = None

        unittest_config_file = cfg.get('runtests', 'unittest_defaults')
        cfg.read(unittest_config_file)

        self.loggerdeco.info('config_file = %s, unittest_config_file = %s' %
                             (config_file, unittest_config_file))

        test_parameters['xre_path'] = cfg.get('runtests', 'xre_path')
        test_parameters['utility_path'] = cfg.get('runtests', 'utility_path')
        if test_parameters['symbols_path'] is not None and cfg.has_option('runtests',
                                                       'minidump_stackwalk'):
            test_parameters['minidump_stackwalk'] = cfg.get('runtests', 'minidump_stackwalk')
            os.environ['MINIDUMP_STACKWALK'] = test_parameters['minidump_stackwalk']
        elif 'MINIDUMP_STACKWALK' in os.environ:
            del os.environ['MINIDUMP_STACKWALK']

        if cfg.has_option('runtests', 'androidprocname'):
            test_parameters['androidprocname'] = cfg.get('runtests', 'androidprocname')

        test_parameters['console_level'] = cfg.get('runtests', 'console_level')
        test_parameters['file_level'] = cfg.get('runtests', 'file_level')
        test_parameters['time_out'] = cfg.getint('runtests', 'time_out')

        if cfg.has_option('runtests', 'iterations'):
            test_parameters['iterations'] = cfg.getint('runtests', 'iterations')
        else:
            test_parameters['iterations'] = 1

        if cfg.has_option('runtests', 'total_chunks'):
            test_parameters['total_chunks'] = cfg.getint('runtests', 'total_chunks')
        else:
            test_parameters['total_chunks'] = 1

        if cfg.has_option('runtests', 'prefs'):
            test_parameters['prefs'] = cfg.get('runtests', 'prefs').split(',')
        else:
            test_parameters['prefs'] = []

        test_parameters['es_server'] = cfg.get('autolog', 'es_server')
        test_parameters['rest_server'] = cfg.get('autolog', 'rest_server')
        test_parameters['index'] = cfg.get('autolog', 'index')
        test_parameters['include_pass'] = cfg.getboolean('autolog', 'include_pass')
        test_parameters['submit_log'] = cfg.getboolean('autolog', 'submit_log')
        test_parameters['use_newparser'] = cfg.getboolean('autolog', 'use_newparser')

    def create_test_args(self, test_parameters):
        args = ['python']

        test_name_lower = test_parameters['test_name'].lower()

        if test_name_lower.startswith('robocoptest'):
            test_args = [
                'mochitest/runtestsremote.py',
                '--robocop=%s' % test_parameters['test_manifest'],
                '--robocop-ids=%s/fennec_ids.txt' % test_parameters['cache_build_dir'],
                '--certificate-path=certs',
                '--close-when-done',
                '--autorun',
                '--console-level=%s' % test_parameters['console_level'],
                '--file-level=%s' % test_parameters['file_level'],
                '--repeat=%d' % test_parameters['iterations'],
            ]
        elif test_name_lower.startswith('mochitest'):
            test_args = [
                'mochitest/runtestsremote.py',
                '--test-manifest=%s' % test_parameters['test_manifest'],
                '--test-path=%s' % test_parameters['test_path'],
                '--certificate-path=certs',
                '--close-when-done',
                '--autorun',
                '--console-level=%s' % test_parameters['console_level'],
                '--file-level=%s' % test_parameters['file_level'],
                '--repeat=%d' % test_parameters['iterations'],
            ]
        elif test_name_lower.startswith('reftest'):
            test_args = [
                'reftest/remotereftest.py',
                '--enable-privilege',
                '--ignore-window-size',
                '--bootstrap',
                '%s' % test_parameters['test_manifest'],
                ]
        elif test_name_lower.startswith('jsreftest'):
            test_args = [
                'reftest/remotereftest.py',
                '--enable-privilege',
                '--ignore-window-size',
                '--bootstrap',
                '--extra-profile-file=jsreftest/tests/user.js',
                '%s' % test_parameters['test_manifest'],
                ]
        elif test_name_lower.startswith('crashtest'):
            test_args = [
                'reftest/remotereftest.py',
                '--enable-privilege',
                '--ignore-window-size',
                '--bootstrap',
                '%s' % test_parameters['test_manifest'],
                ]
        else:
            self.loggerdeco.error('Unknown test_name %s' % test_parameters['test_name'])
            raise Exception('Unknown test_name %s' % test_parameters['test_name'])

        test_parameters['http_port'] = test_parameters['port_manager'].reserve()
        test_parameters['ssl_port'] = test_parameters['port_manager'].reserve()

        common_args = [
            '--deviceIP=%s' % test_parameters['phone_ip_address'],
            '--devicePort=%s' % test_parameters['device_port'],
            '--app=%s' % test_parameters['androidprocname'],
            '--xre-path=%s' % test_parameters['xre_path'],
            '--utility-path=%s' % test_parameters['utility_path'],
            '--timeout=%d' % test_parameters['time_out'],
            '--http-port=%s' % test_parameters['port_manager'].use(test_parameters['http_port']),
            '--ssl-port=%s' % test_parameters['port_manager'].use(test_parameters['ssl_port']),
            '--total-chunks=%d' % test_parameters['total_chunks'],
            '--this-chunk=%d' % test_parameters['this_chunk'],
            '--log-file=%s-%s.log' % (test_parameters['test_name'], test_parameters['phone_ip_address']),
            '--pidfile=%s-%s.pid' % (test_parameters['test_name'], test_parameters['phone_ip_address']),
        ]
        for pref in test_parameters['prefs']:
            common_args.append('--setpref=%s' % pref)

        args.extend(test_args)
        args.extend(common_args)

        if test_parameters['symbols_path'] is not None:
            args.append('--symbols-path=%s' % test_parameters['symbols_path'])

        return args

    def process_test_log(self, test_parameters, logfilehandle):

        test_log = None
        test_runs = []

        if test_parameters['use_newparser']:
            logfilehandle.close()
            logfilehandle = open(logfilehandle.name)
            try:
                # Turn off verbose logging for the log parser
                logger = logging.getLogger('logparser')
                logger_effectiveLevel = logger.getEffectiveLevel()
                logger.setLevel(logging.WARN)
                test_log = newlogparser.parse_log(logfilehandle)
                test_runs = test_log.convert(test_parameters['include_pass'])
            finally:
                logger.setLevel(logger_effectiveLevel)
                logfilehandle.close()
        else:
            lp = LogParser([logfilehandle.name],
                           es=False,
                           es_server=None,
                           includePass=True,
                           output_dir=None,
                           logger=self.logger,
                           harnessType=test_parameters['harness_type'])

            # Use logparser's parsers, but do not allow it to
            # submit data directly to elasticsearch.
            test_runs.append(lp.parseFiles())

        if test_parameters['es_server'] is None or test_parameters['rest_server'] is None:
            return

        # testgroup must match entry in autolog/js/Config.js:testNames
        # os        must match entry in autolog/js/Config.js:OSNames
        # platform  must match entry in autolog/js/Config.js:OSNames

        logfilename = None
        if test_parameters['submit_log']:
            logfilename = logfilehandle.name

        chunk_descriptor = ''
        if test_parameters['total_chunks'] > 1:
            chunk_descriptor = 's-%d' % test_parameters['this_chunk']

        testgroup_name = '%s%s' % (test_parameters['test_name'],
                                   chunk_descriptor)

        platform_name = self.phone_cfg['machinetype']

        self.loggerdeco.debug('testgroup_name = %s' % testgroup_name)

        testgroup = RESTfulAutologTestGroup(
            index=test_parameters['index'],
            testgroup=testgroup_name,
            os='android',
            platform=platform_name,
            harness=test_parameters['harness_type'],
            server=test_parameters['es_server'],
            restserver=test_parameters['rest_server'],
            machine=self.phone_cfg['phoneid'],
            logfile=logfilename)

        testgroup.set_primary_product(
            tree=test_parameters['tree'],
            buildtype='opt',
            buildid=test_parameters['buildid'],
            revision=test_parameters['revision'])

        for testdata in test_runs:

            if self.logger.getEffectiveLevel() == logging.DEBUG:
                self.loggerdeco.debug('Begin testdata')
                self.loggerdeco.debug(json.dumps(testdata, indent=4))
                self.loggerdeco.debug('End testdata')

            testgroup.add_test_suite(
                testsuite=testgroup_name,
                cmdline=test_parameters['cmdline'],
                passed=testdata.get('passed', None),
                failed=testdata.get('failed', None),
                todo=testdata.get('todo', None))

            for t in testdata.get('failures', {}):
                test = t["test"]
                for f in t["failures"]:
                    text = f["text"]
                    status = f["status"]
                    testgroup.add_test_failure(test=test,
                                               text=text,
                                               status=status)

            # Submitting passing tests not supported via REST API
            if test_parameters['include_pass']:
                for t in testdata.get('passes', {}):
                    test = t["test"]
                    duration = None
                    if "duration" in t:
                        duration = t["duration"]
                    testgroup.add_test_pass(test=test,
                                            duration=duration)

        testgroup.submit()

    def runtest(self, test_parameters):

        self.loggerdeco = LogDecorator(self.logger,
                                       {'phoneid': self.phone_cfg['phoneid'],
                                        'phoneip': self.phone_cfg['ip'],
                                        'buildid': test_parameters['buildid'],
                                        'testname': test_parameters['test_name']},
                                       '%(phoneid)s|%(phoneip)s|%(buildid)s|'
                                       '%(testname)s|%(message)s')

        if self.logger.getEffectiveLevel() == logging.DEBUG:
            self.loggerdeco.debug('runtestsremote.py runtest start')
            for key in test_parameters.keys():
                self.loggerdeco.debug('test parameters: %s = %s' %
                                      (key, test_parameters[key]))

        self.set_status(msg='Starting test %s' % test_parameters['test_name'])

        test_parameters['harness_type'] = test_parameters['test_name']

        if test_parameters['test_name'] == 'robocoptest':
            test_parameters['harness_type'] = 'mochitest'
            robocop_apk_path = posixpath.join(self.dm.getDeviceRoot(), 'robocop.apk')

            # XXX: FIXME. When bug 792072 lands, change to have
            # installApp() push the file

            self.dm.pushFile(os.path.join(test_parameters['cache_build_dir'],
                                          'robocop.apk'),
                             robocop_apk_path)
            try:
                self.dm.uninstallApp('org.mozilla.roboexample.test')
            except DMError:
                self.loggerdeco.exception('runtestsremote.py:runtest: Exception running test.')

            self.dm.installApp(robocop_apk_path)
            self.dm.removeFile(robocop_apk_path)

        test_parameters['port_manager'] = PortManager(test_parameters['host_ip_address'])

        for this_chunk in range(1, test_parameters['total_chunks'] + 1):

            test_parameters['this_chunk'] = this_chunk

            try:
                while True:
                    socket_collision = False

                    logfilehandle = tempfile.NamedTemporaryFile(delete=False)
                    self.loggerdeco.debug('logging to %s' % logfilehandle.name)

                    args = self.create_test_args(test_parameters)

                    test_parameters['cmdline'] = ' '.join(args)
                    self.loggerdeco.debug("cmdline = %s" %
                                          test_parameters['cmdline'])

                    self.set_status(msg='Running test %s chunk %d of %d' %
                                    (test_parameters['test_name'],
                                     this_chunk, test_parameters['total_chunks']))
                    if self.dm.processExist(test_parameters['androidprocname']):
                        max_kill_attempts = 3
                        for kill_attempt in range(max_kill_attempts):
                            self.loggerdeco.debug(
                                'Process %s exists. Attempt %d to kill.' % (
                                    test_parameters['androidprocname'], kill_attempt + 1))
                            self.dm.killProcess(test_parameters['androidprocname'])
                            if not self.dm.processExist(test_parameters['androidprocname']):
                                break
                        if kill_attempt == max_kill_attempts - 1 and \
                                self.dm.processExist(test_parameters['androidprocname']):
                            self.loggerdeco.warning(
                                'Could not kill process %s.' % (
                                    test_parameters['androidprocname']))
                    proc = subprocess.Popen(
                        args,
                        cwd=os.path.join(test_parameters['cache_build_dir'],
                                         'tests'),
                        preexec_fn=lambda: os.setpgid(0, 0),
                        stdout=logfilehandle,
                        stderr=subprocess.STDOUT,
                        close_fds=True
                    )
                    proc.wait()
                    self.loggerdeco.debug('runtestsremote.py return code %d' %
                                          proc.returncode)

                    logfilehandle.close()
                    reSocketError = re.compile('socket\.error:')
                    logfilehandle = open(logfilehandle.name)
                    for logline in logfilehandle:
                        if reSocketError.search(logline):
                            socket_collision = True
                            break
                    logfilehandle.close()

                    if not socket_collision:
                        break

                self.set_status(msg='Completed test %s chunk %d of %d' %
                                (test_parameters['test_name'],
                                 this_chunk, test_parameters['total_chunks']))
            except:
                error_message = ('Exception during test %s chunk %d of %d: %s' %
                                 (test_parameters['test_name'],
                                  this_chunk, test_parameters['total_chunks'],
                                  traceback.format_exc()))
                self.set_status(msg=error_message)
                self.loggerdeco.error(error_message)
            finally:
                logfilehandle.close()
                self.process_test_log(test_parameters, logfilehandle)
                if self.logger.getEffectiveLevel() == logging.DEBUG:
                    logfilehandle = open(logfilehandle.name)
                    self.loggerdeco.debug(40 * '*')
                    self.loggerdeco.debug(logfilehandle.read())
                    self.loggerdeco.debug(40 * '-')
                    logfilehandle.close()
                os.unlink(logfilehandle.name)
                # wait for a minute to give the phone time to settle
                time.sleep(60)
                # Recover the phone in between tests/chunks.
                self.loggerdeco.info('Rebooting device after test.')
                self.worker_subprocess.recover_phone()

        self.loggerdeco.debug('runtestsremote.py runtest exit')

        return
Example #32
0
    def __init__(self, dm=None, phone=None, options=None, config_file=None, chunk=1, repos=[]):
        # Ensure that repos is a list and that it is sorted in order
        # for comparisons with the tests loaded from the jobs database
        # to work.
        assert type(repos) == list, 'PhoneTest repos argument must be a list'
        repos.sort()
        self._add_instance(phone.id, config_file, chunk)
        # The default preferences and environment for running fennec
        # are set here in PhoneTest. Tests which subclass PhoneTest can
        # add or override these preferences during their
        # initialization.
        self._preferences = None
        self._environment = None
        self.config_file = config_file
        self.cfg = ConfigParser.ConfigParser()
        # Make the values in the config file case-sensitive
        self.cfg.optionxform = str
        self.cfg.read(self.config_file)
        self.enable_unittests = False
        self.chunk = chunk
        self.chunks = 1
        self.update_status_cb = None
        self.dm = dm
        self.phone = phone
        self.worker_subprocess = None
        self.options = options
        self.loggerdeco = LogDecorator(logger,
                                       {'phoneid': self.phone.id},
                                       '%(phoneid)s|%(message)s')
        self.loggerdeco_original = None
        self.dm_logger_original = None
        self.loggerdeco.info('init autophone.phonetest')
        # test_logfilehandler is used by running tests to save log
        # messages to a separate file which can be reset at the
        # beginning of each test independently of the worker's log.
        self.test_logfilehandler = None
        self._base_device_path = ''
        self.profile_path = '/data/local/tmp/profile'
        self.repos = repos
        self.test_logfile = None
        self.unittest_logpath = None
        # Treeherder related items.
        self._job_name = None
        self._job_symbol = None
        self._group_name = None
        self._group_symbol = None
        self.test_result = PhoneTestResult()
        self.message = None
        # A unique consistent guid is necessary for identifying the
        # test job in treeherder. The test job_guid is updated when a
        # test is added to the pending jobs/tests in the jobs
        # database.
        self.job_guid = None
        self.job_details = []
        self.submit_timestamp = None
        self.start_timestamp = None
        self.end_timestamp = None
        self.logcat = Logcat(self, self.loggerdeco)
        self.loggerdeco.debug('PhoneTest: %s, cfg sections: %s' % (self.__dict__, self.cfg.sections()))
        if not self.cfg.sections():
            self.loggerdeco.warning('Test configuration not found. '
                                    'Will use defaults.')
        # upload_dir will hold ANR traces, tombstones and other files
        # pulled from the device.
        self.upload_dir = None
        # crash_processor is an instance of AutophoneCrashProcessor that
        # is used by non-unittests to process device errors and crashes.
        self.crash_processor = None
        # Instrument running time
        self.start_time = None
        self.stop_time = None
        # Perform initial configuration. For tests which do not
        # specify all config options, reasonable defaults will be
        # chosen.

        # [paths]
        self.autophone_directory = os.path.dirname(os.path.abspath(sys.argv[0]))
        self._paths = {}
        self._paths['dest'] = posixpath.join(self.base_device_path,
                                             self.__class__.__name__)
        try:
            sources = self.cfg.get('paths', 'sources').split()
            self._paths['sources'] = []
            for source in sources:
                if not source.endswith('/'):
                    source += '/'
                self._paths['sources'].append(source)
        except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
            self._paths['sources'] = [
                os.path.join(self.autophone_directory, 'files/base/')]
        try:
            self._paths['dest'] = self.cfg.get('paths', 'dest')
            if not self._paths['dest'].endswith('/'):
                self._paths['dest'] += '/'
        except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
            pass
        try:
            self._paths['profile'] = self.cfg.get('paths', 'profile')
            if not self._paths['profile'].endswith('/'):
                self._paths['profile'] += '/'
        except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
            pass
        if 'profile' in self._paths:
            self.profile_path = self._paths['profile']
        # _pushes = {'sourcepath' : 'destpath', ...}
        self._pushes = {}
        for source in self._paths['sources']:
            for push in glob.glob(source + '*'):
                if push.endswith('~') or push.endswith('.bak'):
                    continue
                push_dest = posixpath.join(self._paths['dest'],
                                           os.path.basename(push))
                self._pushes[push] = push_dest
        self._initialize_url = os.path.join('file://', self._paths['dest'],
                                            'initialize_profile.html')
        # [tests]
        self._tests = {}
        try:
            for t in self.cfg.items('tests'):
                self._tests[t[0]] = t[1]
        except ConfigParser.NoSectionError:
            self._tests['blank'] = 'blank.html'

        self.loggerdeco.info('PhoneTest: Connected.')
Example #33
0
    def runtest(self, test_parameters):

        self.loggerdeco = LogDecorator(self.logger,
                                       {'phoneid': self.phone_cfg['phoneid'],
                                        'phoneip': self.phone_cfg['ip'],
                                        'buildid': test_parameters['buildid'],
                                        'testname': test_parameters['test_name']},
                                       '%(phoneid)s|%(phoneip)s|%(buildid)s|'
                                       '%(testname)s|%(message)s')

        if self.logger.getEffectiveLevel() == logging.DEBUG:
            self.loggerdeco.debug('runtestsremote.py runtest start')
            for key in test_parameters.keys():
                self.loggerdeco.debug('test parameters: %s = %s' %
                                      (key, test_parameters[key]))

        self.set_status(msg='Starting test %s' % test_parameters['test_name'])

        test_parameters['harness_type'] = test_parameters['test_name']

        if test_parameters['test_name'] == 'robocoptest':
            test_parameters['harness_type'] = 'mochitest'
            robocop_apk_path = posixpath.join(self.dm.getDeviceRoot(), 'robocop.apk')

            # XXX: FIXME. When bug 792072 lands, change to have
            # installApp() push the file

            self.dm.pushFile(os.path.join(test_parameters['cache_build_dir'],
                                          'robocop.apk'),
                             robocop_apk_path)
            try:
                self.dm.uninstallApp('org.mozilla.roboexample.test')
            except DMError:
                self.loggerdeco.exception('runtestsremote.py:runtest: Exception running test.')

            self.dm.installApp(robocop_apk_path)
            self.dm.removeFile(robocop_apk_path)

        test_parameters['port_manager'] = PortManager(test_parameters['host_ip_address'])

        for this_chunk in range(1, test_parameters['total_chunks'] + 1):

            test_parameters['this_chunk'] = this_chunk

            try:
                while True:
                    socket_collision = False

                    logfilehandle = tempfile.NamedTemporaryFile(delete=False)
                    self.loggerdeco.debug('logging to %s' % logfilehandle.name)

                    args = self.create_test_args(test_parameters)

                    test_parameters['cmdline'] = ' '.join(args)
                    self.loggerdeco.debug("cmdline = %s" %
                                          test_parameters['cmdline'])

                    self.set_status(msg='Running test %s chunk %d of %d' %
                                    (test_parameters['test_name'],
                                     this_chunk, test_parameters['total_chunks']))
                    if self.dm.processExist(test_parameters['androidprocname']):
                        max_kill_attempts = 3
                        for kill_attempt in range(max_kill_attempts):
                            self.loggerdeco.debug(
                                'Process %s exists. Attempt %d to kill.' % (
                                    test_parameters['androidprocname'], kill_attempt + 1))
                            self.dm.killProcess(test_parameters['androidprocname'])
                            if not self.dm.processExist(test_parameters['androidprocname']):
                                break
                        if kill_attempt == max_kill_attempts - 1 and \
                                self.dm.processExist(test_parameters['androidprocname']):
                            self.loggerdeco.warning(
                                'Could not kill process %s.' % (
                                    test_parameters['androidprocname']))
                    proc = subprocess.Popen(
                        args,
                        cwd=os.path.join(test_parameters['cache_build_dir'],
                                         'tests'),
                        preexec_fn=lambda: os.setpgid(0, 0),
                        stdout=logfilehandle,
                        stderr=subprocess.STDOUT,
                        close_fds=True
                    )
                    proc.wait()
                    self.loggerdeco.debug('runtestsremote.py return code %d' %
                                          proc.returncode)

                    logfilehandle.close()
                    reSocketError = re.compile('socket\.error:')
                    logfilehandle = open(logfilehandle.name)
                    for logline in logfilehandle:
                        if reSocketError.search(logline):
                            socket_collision = True
                            break
                    logfilehandle.close()

                    if not socket_collision:
                        break

                self.set_status(msg='Completed test %s chunk %d of %d' %
                                (test_parameters['test_name'],
                                 this_chunk, test_parameters['total_chunks']))
            except:
                error_message = ('Exception during test %s chunk %d of %d: %s' %
                                 (test_parameters['test_name'],
                                  this_chunk, test_parameters['total_chunks'],
                                  traceback.format_exc()))
                self.set_status(msg=error_message)
                self.loggerdeco.error(error_message)
            finally:
                logfilehandle.close()
                self.process_test_log(test_parameters, logfilehandle)
                if self.logger.getEffectiveLevel() == logging.DEBUG:
                    logfilehandle = open(logfilehandle.name)
                    self.loggerdeco.debug(40 * '*')
                    self.loggerdeco.debug(logfilehandle.read())
                    self.loggerdeco.debug(40 * '-')
                    logfilehandle.close()
                os.unlink(logfilehandle.name)
                # wait for a minute to give the phone time to settle
                time.sleep(60)
                # Recover the phone in between tests/chunks.
                self.loggerdeco.info('Rebooting device after test.')
                self.worker_subprocess.recover_phone()

        self.loggerdeco.debug('runtestsremote.py runtest exit')

        return
Example #34
0
    def runtests(self, build_metadata, worker_subprocess):
        self.loggerdeco = LogDecorator(self.logger,
                                       {'phoneid': self.phone_cfg['phoneid'],
                                        'phoneip': self.phone_cfg['ip'],
                                        'buildid': build_metadata['buildid']},
                                       '%(phoneid)s|%(phoneip)s|%(buildid)s|'
                                       '%(message)s')
        self.dm._logger = self.loggerdeco
        appname = build_metadata['androidprocname']

        if not self.install_local_pages():
            self.set_status(msg='Could not install local pages on phone. '
                            'Aborting test for '
                            'build %s' % build_metadata['buildid'])
            return

        if not self.create_profile(build_metadata):
            self.set_status(msg='Could not run Fennec. Aborting test for '
                            'build %s' % build_metadata['buildid'])
            return

        testcount = len(self._urls.keys())
        for testnum,(testname,url) in enumerate(self._urls.iteritems(), 1):
            self.loggerdeco = LogDecorator(self.logger,
                                           {'phoneid': self.phone_cfg['phoneid'],
                                            'phoneip': self.phone_cfg['ip'],
                                            'buildid': build_metadata['buildid'],
                                            'testname': testname},
                                           '%(phoneid)s|%(phoneip)s|%(buildid)s|'
                                           '%(testname)s|%(message)s')
            self.dm._logger = self.loggerdeco
            if self.check_results(build_metadata, testname):
                # We already have good results for this test and build.
                # No need to test it again.
                self.loggerdeco.info('Skipping test (%d/%d) for %d iterations' %
                                     (testnum, testcount, self._iterations))
                continue
            self.loggerdeco.info('Running test (%d/%d) for %d iterations' %
                                 (testnum, testcount, self._iterations))

            for attempt in range(self.stderrp_attempts):
                # dataset is a list of the measurements made for the
                # iterations for this test.
                #
                # An empty item in the dataset list represents a
                # failure to obtain any measurement for that
                # iteration.
                #
                # It is possible for an item in the dataset to have an
                # uncached value and not have a corresponding cached
                # value if the cached test failed to record the
                # values.

                dataset = []
                for iteration in range(self._iterations):
                    self.set_status(msg='Attempt %d/%d for Test %d/%d, '
                                    'run %d, for url %s' %
                                    (attempt+1, self.stderrp_attempts,
                                     testnum, testcount, iteration+1, url))

                    dataset.append({})

                    if not self.create_profile(build_metadata):
                        continue

                    measurement = self.runtest(build_metadata, appname, url)
                    if not measurement:
                        continue
                    dataset[-1]['uncached'] = measurement

                    measurement = self.runtest(build_metadata, appname, url)
                    if not measurement:
                        continue
                    dataset[-1]['cached'] = measurement

                    if self.is_stderr_below_threshold(dataset,
                                                      self.stderrp_accept):
                        self.loggerdeco.info(
                            'Accepted test (%d/%d) after %d of %d iterations' %
                            (testnum, testcount, iteration+1, self._iterations))
                        break

                self.loggerdeco.debug('publishing results')

                if self.is_stderr_below_threshold(dataset, self.stderrp_reject):
                    rejected = False
                else:
                    rejected = True
                    self.loggerdeco.info(
                        'Rejected test (%d/%d) after %d/%d iterations' %
                        (testnum, testcount, iteration+1, self._iterations))

                for datapoint in dataset:
                    for cachekey in datapoint:
                        self.publish_results(
                            starttime=datapoint[cachekey]['starttime'],
                            tstrt=datapoint[cachekey]['throbberstart'],
                            tstop=datapoint[cachekey]['throbberstop'],
                            build_metadata=build_metadata,
                            testname=testname,
                            cache_enabled=(cachekey=='cached'),
                            rejected=rejected)
                if not rejected:
                    break
Example #35
0
    def run_job(self):
        if not self.install_local_pages():
            self.test_failure(
                self.name, 'TEST_UNEXPECTED_FAIL',
                'Aborting test - Could not install local pages on phone.',
                PhoneTestResult.EXCEPTION)
            return

        if not self.create_profile():
            self.test_failure(
                self.name, 'TEST_UNEXPECTED_FAIL',
                'Aborting test - Could not run Fennec.',
                PhoneTestResult.BUSTED)
            return

        testcount = len(self._urls.keys())
        for testnum,(testname,url) in enumerate(self._urls.iteritems(), 1):
            if self.fennec_crashed:
                break
            self.loggerdeco = LogDecorator(logger,
                                           {'phoneid': self.phone.id,
                                            'buildid': self.build.id,
                                            'testname': testname},
                                           '%(phoneid)s|%(buildid)s|'
                                           '%(testname)s|%(message)s')
            self.dm._logger = self.loggerdeco
            self.loggerdeco.info('Running test (%d/%d) for %d iterations' %
                                 (testnum, testcount, self._iterations))

            # success == False indicates that none of the attempts
            # were successful in getting any measurement. This is
            # typically due to a regression in the brower which should
            # be reported.
            success = False
            command = None
            for attempt in range(1, self.stderrp_attempts+1):
                if self.fennec_crashed:
                    break
                # dataset is a list of the measurements made for the
                # iterations for this test.
                #
                # An empty item in the dataset list represents a
                # failure to obtain any measurement for that
                # iteration.
                #
                # It is possible for an item in the dataset to have an
                # uncached value and not have a corresponding cached
                # value if the cached test failed to record the
                # values.

                dataset = []
                for iteration in range(1, self._iterations+1):
                    command = self.worker_subprocess.process_autophone_cmd(self)
                    if command['interrupt']:
                        self.handle_test_interrupt(command['reason'])
                        break
                    if self.fennec_crashed:
                        break
                    self.update_status(message='Attempt %d/%d for Test %d/%d, '
                                       'run %d, for url %s' %
                                       (attempt, self.stderrp_attempts,
                                        testnum, testcount, iteration, url))

                    dataset.append({})

                    if not self.create_profile():
                        self.test_failure(url,
                                          'TEST_UNEXPECTED_FAIL',
                                          'Failed to create profile',
                                          PhoneTestResult.TESTFAILED)
                        continue

                    measurement = self.runtest(url)
                    if measurement:
                        self.test_pass(url)
                    else:
                        self.test_failure(
                            url,
                            'TEST_UNEXPECTED_FAIL',
                            'Failed to get uncached measurement.',
                            PhoneTestResult.TESTFAILED)
                        continue
                    dataset[-1]['uncached'] = measurement
                    success = True

                    measurement = self.runtest(url)
                    if measurement:
                        self.test_pass(url)
                    else:
                        self.test_failure(
                            url,
                            'TEST_UNEXPECTED_FAIL',
                            'Failed to get cached measurement.',
                            PhoneTestResult.TESTFAILED)
                        continue
                    dataset[-1]['cached'] = measurement

                    if self.is_stderr_below_threshold(
                            ('throbberstart',
                             'throbberstop'),
                            dataset,
                            self.stderrp_accept):
                        self.loggerdeco.info(
                            'Accepted test (%d/%d) after %d of %d iterations' %
                            (testnum, testcount, iteration, self._iterations))
                        break

                if command and command['interrupt']:
                    break
                if not success:
                    # If we have not gotten a single measurement at this point,
                    # just bail and report the failure rather than wasting time
                    # continuing more attempts.
                    self.loggerdeco.info(
                        'Failed to get measurements for test %s after %d/%d attempt '
                        'of %d iterations' % (testname, attempt,
                                              self.stderrp_attempts,
                                              self._iterations))
                    self.worker_subprocess.mailer.send(
                        'S1S2Test %s failed for Build %s %s on Phone %s' %
                        (testname, self.build.tree, self.build.id,
                         self.phone.id),
                        'No measurements were detected for test %s.\n\n'
                        'Repository: %s\n'
                        'Build Id:   %s\n'
                        'Revision:   %s\n' %
                        (testname, self.build.tree, self.build.id, self.build.revision))
                    self.test_failure(self.name, 'TEST_UNEXPECTED_FAIL',
                                      'No measurements detected.',
                                      PhoneTestResult.BUSTED)
                    break

                if self.is_stderr_below_threshold(
                        ('throbberstart',
                         'throbberstop'),
                        dataset,
                        self.stderrp_reject):
                    rejected = False
                else:
                    rejected = True
                    self.loggerdeco.info(
                        'Rejected test (%d/%d) after %d/%d iterations' %
                        (testnum, testcount, iteration, self._iterations))

                self.loggerdeco.debug('publishing results')

                for datapoint in dataset:
                    for cachekey in datapoint:
                        self.report_results(
                            starttime=datapoint[cachekey]['starttime'],
                            tstrt=datapoint[cachekey]['throbberstart'],
                            tstop=datapoint[cachekey]['throbberstop'],
                            testname=testname,
                            cache_enabled=(cachekey=='cached'),
                            rejected=rejected)
                if not rejected:
                    break

            if command and command['interrupt']:
                break
            elif not success:
                break
Example #36
0
    def runjob(self, build_metadata, worker_subprocess):
        logger = self.logger
        loggerdeco = self.loggerdeco
        self.logger = logging.getLogger('autophone.worker.subprocess.test')
        self.loggerdeco = LogDecorator(self.logger,
                                       {'phoneid': self.phone_cfg['phoneid'],
                                        'phoneip': self.phone_cfg['ip'],
                                        'buildid': build_metadata['buildid']},
                                       '%(phoneid)s|%(phoneip)s|%(buildid)s|'
                                       '%(message)s')
        self.dm._logger = self.loggerdeco

        try:
            # Read our config file which gives us our number of
            # iterations and urls that we will be testing
            cfg = ConfigParser.RawConfigParser()
            cfg.read(self.config_file)
            # [signature]
            self._signer = None
            self._jwt = {'id': '', 'key': None}
            for opt in self._jwt.keys():
                try:
                    self._jwt[opt] = cfg.get('signature', opt)
                except (ConfigParser.NoSectionError,
                        ConfigParser.NoOptionError):
                    break
            # phonedash requires both an id and a key.
            if self._jwt['id'] and self._jwt['key']:
                self._signer = jwt.jws.HmacSha(key=self._jwt['key'],
                                               key_id=self._jwt['id'])
            # [paths]
            autophone_directory = os.path.dirname(os.path.abspath(sys.argv[0]))
            self._paths = {}
            self._paths['source'] = os.path.join(autophone_directory, 'files/')
            self._paths['dest'] = posixpath.join(self.base_device_path, 's1test/')
            self._paths['remote'] = 'http://phonedash.mozilla.org/'
            try:
                for opt in ('source', 'dest', 'profile', 'remote'):
                    try:
                        self._paths[opt] = cfg.get('paths', opt)
                        if not self._paths[opt].endswith('/'):
                            self._paths[opt] += '/'
                    except ConfigParser.NoOptionError:
                        pass
            except ConfigParser.NoSectionError:
                pass
            if 'profile' in self._paths:
                self.profile_path = self._paths['profile']
            # _pushes = {'sourcepath' : 'destpath', ...}
            self._pushes = {}
            for push in glob.glob(self._paths['source'] + '*'):
                if push.endswith('~') or push.endswith('.bak'):
                    continue
                push_dest = posixpath.join(self._paths['dest'], os.path.basename(push))
                self._pushes[push] = push_dest
            # [tests]
            self._tests = {}
            for t in cfg.items('tests'):
                self._tests[t[0]] = t[1]
            # Map URLS - {urlname: url} - urlname serves as testname
            self._urls = {}
            for test_location in ('local', 'remote'):
                for test_name in self._tests:
                    if test_location == 'local':
                        test_url = 'file://' + self._paths['dest'] + self._tests[test_name]
                    else:
                        test_url = self._paths['remote'] + self._tests[test_name]
                    self._urls["%s-%s" % (test_location, test_name)] = test_url
            # [settings]
            self._iterations = cfg.getint('settings', 'iterations')
            try:
                self.stderrp_accept = cfg.getfloat('settings', 'stderrp_accept')
            except ConfigParser.NoOptionError:
                self.stderrp_accept = 0
            try:
                self.stderrp_reject = cfg.getfloat('settings', 'stderrp_reject')
            except ConfigParser.NoOptionError:
                self.stderrp_reject = 100
            try:
                self.stderrp_attempts = cfg.getint('settings', 'stderrp_attempts')
            except ConfigParser.NoOptionError:
                self.stderrp_attempts = 1
            self._resulturl = cfg.get('settings', 'resulturl')
            if not self._resulturl.endswith('/'):
                self._resulturl += '/'
            self._initialize_url = 'file://' + self._paths['dest'] + 'initialize_profile.html'

            self.runtests(build_metadata, worker_subprocess)
        finally:
            self.logger = logger
            self.loggerdeco = loggerdeco
            self.dm._logger = loggerdeco
Example #37
0
class UnitTest(PhoneTest):
    def __init__(self, phone, options, config_file=None,
                 enable_unittests=False, test_devices_repos={},
                 chunk=1):
        PhoneTest.__init__(self, phone, options,
                           config_file=config_file,
                           enable_unittests=enable_unittests,
                           test_devices_repos=test_devices_repos,
                           chunk=chunk)
        self.unittest_cfg = ConfigParser.RawConfigParser()

        unittest_config_file = self.cfg.get('runtests', 'unittest_defaults')
        self.unittest_cfg.read(unittest_config_file)

        self.loggerdeco.info('config_file = %s, unittest_config_file = %s' %
                             (config_file, unittest_config_file))

        # Mochitests in particular are broken when run via adb. We must
        # use the SUTAgent and will need the phone's ip address.
        phone_ip_address = None
        for attempt in range(1, self.options.phone_retry_limit+1):
            phone_ip_address = self.dm.get_ip_address()
            self.loggerdeco.debug(
                'UnitTest: get phone ip address Attempt: %d: %s' %
                (attempt, phone_ip_address))
            if phone_ip_address:
                break
            time.sleep(self.options.phone_retry_wait)
        if not phone_ip_address:
            raise Exception('UnitTest: Failed to get phone %s ip address' % self.phone.id)

        self.parms = {
            'host_ip_address': self.phone.host_ip,
            'phone_ip_address': phone_ip_address,
            'phoneid': self.phone.id,
            'config_file': config_file,
            'test_name': self.cfg.get('runtests', 'test_name'),
            'test_manifest': self.cfg.get('runtests', 'test_manifest'),
        }

        self.parms['xre_path'] = self.unittest_cfg.get('runtests', 'xre_path')
        self.parms['utility_path'] = self.unittest_cfg.get('runtests', 'utility_path')
        if self.unittest_cfg.has_option('runtests', 'include_pass'):
            self.parms['include_pass'] = self.unittest_cfg.getboolean('runtests', 'include_pass')
        else:
            self.parms['include_pass'] = False

        if self.cfg.has_option('runtests', 'app_name'):
            self.parms['app_name'] = self.cfg.get('runtests', 'app_name')

        self.parms['console_level'] = self.unittest_cfg.get('runtests', 'console_level')
        self.parms['log_level'] = self.unittest_cfg.get('runtests', 'log_level')
        self.parms['time_out'] = self.unittest_cfg.getint('runtests', 'time_out')

        if self.cfg.has_option('runtests', 'iterations'):
            self.parms['iterations'] = self.cfg.getint('runtests', 'iterations')
        else:
            self.parms['iterations'] = 1

        if self.cfg.has_option('runtests', 'total_chunks'):
            self.chunks = self.cfg.getint('runtests', 'total_chunks')

        if self.cfg.has_option('runtests', 'prefs'):
            self.parms['prefs'] = self.cfg.get('runtests', 'prefs').split(',')
        else:
            self.parms['prefs'] = []

    @property
    def name(self):
        return 'autophone-%s%s' % (self.parms['test_name'], self.name_suffix)

    def setup_job(self):
        PhoneTest.setup_job(self)
        build_dir = self.build.dir
        symbols_path = self.build.symbols
        if symbols_path and not os.path.exists(symbols_path):
            symbols_path = None
        re_revision = re.compile(r'http.*/rev/(.*)')
        match = re_revision.match(self.build.revision)
        if match:
            revision = match.group(1)
        else:
            revision = self.build.revision

        self.parms['app_name'] = self.build.app_name
        self.parms['build_dir'] = build_dir
        self.parms['symbols_path'] = symbols_path
        self.parms['revision'] = revision
        self.parms['buildid'] = self.build.id
        self.parms['tree'] = self.build.tree

        self._log = '%s/tests/%s-%s-%s.log' % (build_dir,
                                               self.parms['test_name'],
                                               self.chunk,
                                               self.parms['phoneid'])
        os.putenv('MINIDUMP_STACKWALK', self.options.minidump_stackwalk)
        os.putenv('MINIDUMP_SAVE_PATH', self.upload_dir)
        os.putenv('MOZ_UPLOAD_DIR', self.upload_dir)

    def teardown_job(self):
        os.unsetenv('MINIDUMP_STACKWALK')
        os.unsetenv('MINIDUMP_SAVE_PATH')
        os.unsetenv('MOZ_UPLOAD_DIR')
        PhoneTest.teardown_job(self)

    def run_job(self):
        self.loggerdeco.debug('runtestsremote.py run_job start')
        self.update_status(message='runtestsremote.py run_job start')

        self.worker_subprocess.check_sdcard()

        if self.logger.getEffectiveLevel() == logging.DEBUG:
            self.loggerdeco.debug('phone = %s' % self.phone)

        if not self.cfg.has_option('runtests', 'test_name'):
            raise Exception('Job configuration %s does not specify a test' %
                            self.config_file)
        try:
            self.runtest()
        except:
            # This exception handler deals with exceptions which occur outside
            # of the actual test runner. Exceptions from the test runner
            # are handled locally in runtest.
            self.loggerdeco.exception('runtestsremote.py:run_job: Exception '
                                      'running test')
            self.update_status(message='runtestsremote.py:run_job: Exception '
                               'running test')
            # give the phone a minute to recover
            time.sleep(60)
            self.worker_subprocess.recover_phone()

        self.loggerdeco.debug('runtestsremote.py run_job exit')
        self.update_status(message='runtestsremote.py run_job exit')

    def create_test_args(self):
        args = ['python', '-u']

        test_name_lower = self.parms['test_name'].lower()

        if test_name_lower.startswith('robocoptest'):
            self.parms['harness_type'] = 'mochitest'

            test_args = [
                'mochitest/runtestsremote.py',
                '--robocop=%s' % self.parms['test_manifest'],
                '--robocop-ids=%s/fennec_ids.txt' % self.parms['build_dir'],
                '--certificate-path=certs',
                '--close-when-done',
                '--autorun',
                '--console-level=%s' % self.parms['console_level'],
                '--log-raw=%s' % 'raw-log-' + os.path.basename(self._log),
            ]
        elif test_name_lower.startswith('mochitest'):
            self.parms['harness_type'] = 'mochitest'

            test_args = [
                'mochitest/runtestsremote.py',
                '--manifest=%s' % self.parms['test_manifest'],
                '--testrun-manifest-file=%s-%s-%s-tests.json' % (self.parms['test_name'],
                                                                 self.chunk,
                                                                 self.parms['phoneid']),
                '--certificate-path=certs',
                '--close-when-done',
                '--autorun',
                '--console-level=%s' % self.parms['console_level'],
                '--log-raw=%s' % 'raw-log-' + os.path.basename(self._log),
            ]
        elif test_name_lower.startswith('reftest'):
            self.parms['harness_type'] = 'reftest'

            test_args = [
                'reftest/remotereftest.py',
                '--ignore-window-size',
                '--bootstrap',
                '%s' % self.parms['test_manifest'],
                ]
        elif test_name_lower.startswith('jsreftest'):
            self.parms['harness_type'] = 'reftest'

            test_args = [
                'reftest/remotereftest.py',
                '--ignore-window-size',
                '--bootstrap',
                '--extra-profile-file=jsreftest/tests/user.js',
                '%s' % self.parms['test_manifest'],
                ]
        elif test_name_lower.startswith('crashtest'):
            self.parms['harness_type'] = 'reftest'

            test_args = [
                'reftest/remotereftest.py',
                '--ignore-window-size',
                '--bootstrap',
                '%s' % self.parms['test_manifest'],
                ]
        else:
            self.loggerdeco.error('Unknown test_name %s' % self.parms['test_name'])
            raise Exception('Unknown test_name %s' % self.parms['test_name'])

        if self.parms['iterations'] > 1:
                test_args.append('--repeat=%d' % (self.parms['iterations']-1))

        self.parms['http_port'] = self.parms['port_manager'].reserve()
        self.parms['ssl_port'] = self.parms['port_manager'].reserve()

        common_args = [
            '--dm_trans=sut',
            '--deviceIP=%s' % self.parms['phone_ip_address'],
            '--devicePort=20701',
            '--app=%s' % self.parms['app_name'],
            '--xre-path=%s' % self.parms['xre_path'],
            '--utility-path=%s' % self.parms['utility_path'],
            '--timeout=%d' % self.parms['time_out'],
            '--remote-webserver=%s' % self.parms['host_ip_address'],
            '--http-port=%s' % self.parms['port_manager'].use(self.parms['http_port']),
            '--ssl-port=%s' % self.parms['port_manager'].use(self.parms['ssl_port']),
            '--total-chunks=%d' % self.chunks,
            '--this-chunk=%d' % self.chunk,
            '--pidfile=%s-%s-%s.pid' % (self.parms['test_name'], self.chunk, self.parms['phoneid']),
        ]
        for pref in self.parms['prefs']:
            common_args.append('--setpref=%s' % pref)

        args.extend(test_args)
        args.extend(common_args)

        if self.parms['symbols_path'] is not None:
            args.append('--symbols-path=%s' % self.parms['symbols_path'])

        return args

    def process_test_log(self, logfilehandle):

        logfilehandle.close()

        # convert embedded \n into real newlines
        logfilehandle = open(self._log)
        self.loggerdeco.debug('process_test_log: name: %s, self._log: %s' % (
            logfilehandle.name, self._log))
        logcontents = logfilehandle.read()
        logfilehandle.close()
        logcontents = re.sub(r'\\n', '\n', logcontents)
        logfilehandle = open(self._log, 'wb')
        logfilehandle.write(logcontents)
        logfilehandle.close()
        self.loggerdeco.debug('process_test_log: raw log:\n%s\n' % logcontents)

        lp = LogParser([logfilehandle.name],
                       includePass=True,
                       output_dir=None,
                       logger=self.logger,
                       harnessType=self.parms['harness_type'])
        parsed_log = lp.parseFiles()
        self.loggerdeco.debug('process_test_log: LogParser parsed log : %s' %
                              json.dumps(parsed_log, indent=2))

        self.test_result.todo = parsed_log.get('todo', 0)
        self.test_result.passes = parsed_log.get('passes', [])
        failures = parsed_log.get('failures', [])
        if failures:
            self.test_result.status = PhoneTestResult.TESTFAILED
            for failure in failures:
                for test_failure in failure['failures']:
                    self.test_result.add_failure(failure['test'],
                                                 test_failure['status'],
                                                 test_failure['text'])
        self.loggerdeco.debug('process_test_log: test_result: %s' %
                              json.dumps(self.test_result.__dict__, indent=2))

    def runtest(self):

        self.loggerdeco = LogDecorator(self.logger,
                                       {'phoneid': self.phone.id,
                                        'buildid': self.parms['buildid'],
                                        'testname': self.parms['test_name']},
                                       '%(phoneid)s|%(buildid)s|'
                                       '%(testname)s|%(message)s')

        if self.logger.getEffectiveLevel() == logging.DEBUG:
            self.loggerdeco.debug('runtestsremote.py runtest start')
            for key in self.parms.keys():
                self.loggerdeco.debug('test parameters: %s = %s' %
                                      (key, self.parms[key]))

        self.update_status(message='Starting test %s' % self.parms['test_name'])

        if self.parms['test_name'] == 'robocoptest':
            try:
                self.dm.uninstall_app('org.mozilla.roboexample.test')
                robocop_apk_path = os.path.join(self.parms['build_dir'], 'robocop.apk')
                self.dm.install_app(robocop_apk_path)
            except Exception, e:
                self.loggerdeco.exception('runtestsremote.py:runtest: Exception running test.')
                self.test_result.status = PhoneTestResult.EXCEPTION
                self.message = 'Exception installing robocop.apk: %s' % e
                with open(self._log, "w") as logfilehandle:
                    logfilehandle.write('%s\n' % self.message)
                return

        self.parms['port_manager'] = PortManager(self.parms['host_ip_address'])

        try:
            while True:
                socket_collision = False

                logfilehandle = open(self._log, 'wb')
                self.loggerdeco.debug('logging to %s' % logfilehandle.name)

                args = self.create_test_args()

                self.parms['cmdline'] = ' '.join(args)
                self.loggerdeco.debug("cmdline = %s" %
                                      self.parms['cmdline'])

                self.update_status(message='Running test %s chunk %d of %d' %
                                   (self.parms['test_name'],
                                    self.chunk, self.chunks))
                if self.dm.process_exist(self.parms['app_name']):
                    max_kill_attempts = 3
                    for kill_attempt in range(1, max_kill_attempts+1):
                        self.loggerdeco.debug(
                            'Process %s exists. Attempt %d to kill.' % (
                                self.parms['app_name'], kill_attempt + 1))
                        self.dm.pkill(self.parms['app_name'], root=True)
                        if not self.dm.process_exist(self.parms['app_name']):
                            break
                    if kill_attempt == max_kill_attempts and \
                            self.dm.process_exist(self.parms['app_name']):
                        self.loggerdeco.warning(
                            'Could not kill process %s.' % (
                                self.parms['app_name']))
                proc = subprocess.Popen(
                    args,
                    cwd=os.path.join(self.parms['build_dir'],
                                     'tests'),
                    preexec_fn=lambda: os.setpgid(0, 0),
                    stdout=logfilehandle,
                    stderr=subprocess.STDOUT,
                    close_fds=True
                )
                proc.wait()
                if proc.returncode == 0:
                    self.test_result.status = PhoneTestResult.SUCCESS
                else:
                    self.test_result.status = PhoneTestResult.EXCEPTION
                    self.message = 'Test exited with return code %d' % proc.returncode

                self.loggerdeco.debug('runtestsremote.py return code %d' %
                                      proc.returncode)

                logfilehandle.close()
                # XXX: investigate if this is still needed.
                re_socket_error = re.compile('socket\.error:')
                logfilehandle = open(self._log)
                logcontents = logfilehandle.read()
                logfilehandle.close()
                if re_socket_error.search(logcontents):
                    socket_collision = True

                if not socket_collision:
                    break

            self.update_status(message='Completed test %s chunk %d of %d' %
                               (self.parms['test_name'],
                                self.chunk, self.chunks))
        except:
            logfilehandle.close()
            error_message = ('Exception during test %s chunk %d of %d: %s' %
                             (self.parms['test_name'],
                              self.chunk, self.chunks,
                              traceback.format_exc()))
            self.update_status(message=error_message)
            self.loggerdeco.error(error_message)
            self.test_result.status = PhoneTestResult.EXCEPTION
            self.message = error_message
        finally:
            self.process_test_log(logfilehandle)

        self.loggerdeco.debug('runtestsremote.py runtest exit')

        return