• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Shell class for a test, inherited by all individual tests
2#
3# Methods:
4#       __init__        initialise
5#       initialize      run once for each job
6#       setup           run once for each new version of the test installed
7#       run             run the test (wrapped by job.run_test())
8#
9# Data:
10#       job             backreference to the job this test instance is part of
11#       outputdir       eg. results/<job>/<testname.tag>
12#       resultsdir      eg. results/<job>/<testname.tag>/results
13#       profdir         eg. results/<job>/<testname.tag>/profiling
14#       debugdir        eg. results/<job>/<testname.tag>/debug
15#       bindir          eg. tests/<test>
16#       src             eg. tests/<test>/src
17#       tmpdir          eg. tmp/<tempname>_<testname.tag>
18
19#pylint: disable=C0111
20
21import fcntl
22import json
23import logging
24import os
25import re
26import shutil
27import stat
28import sys
29import tempfile
30import time
31import traceback
32
33from autotest_lib.client.bin import utils
34from autotest_lib.client.common_lib import error
35from autotest_lib.client.common_lib import utils as client_utils
36
37try:
38    from chromite.lib import metrics
39except ImportError:
40    metrics = client_utils.metrics_mock
41
42
43class base_test(object):
44    preserve_srcdir = False
45
46    def __init__(self, job, bindir, outputdir):
47        self.job = job
48        self.pkgmgr = job.pkgmgr
49        self.autodir = job.autodir
50        self.outputdir = outputdir
51        self.tagged_testname = os.path.basename(self.outputdir)
52        self.resultsdir = os.path.join(self.outputdir, 'results')
53        os.mkdir(self.resultsdir)
54        self.profdir = os.path.join(self.outputdir, 'profiling')
55        os.mkdir(self.profdir)
56        self.debugdir = os.path.join(self.outputdir, 'debug')
57        os.mkdir(self.debugdir)
58        # TODO(ericli): figure out how autotest crash handler work with cros
59        # Once this is re-enabled import getpass. crosbug.com/31232
60        # crash handler, we should restore it in near term.
61        # if getpass.getuser() == 'root':
62        #     self.configure_crash_handler()
63        # else:
64        self.crash_handling_enabled = False
65        self.bindir = bindir
66        self.srcdir = os.path.join(self.bindir, 'src')
67        self.tmpdir = tempfile.mkdtemp("_" + self.tagged_testname,
68                                       dir=job.tmpdir)
69        self._keyvals = []
70        self._new_keyval = False
71        self.failed_constraints = []
72        self.iteration = 0
73        self.before_iteration_hooks = []
74        self.after_iteration_hooks = []
75
76        # Flag to indicate if the test has succeeded or failed.
77        self.success = False
78
79
80    def configure_crash_handler(self):
81        pass
82
83
84    def crash_handler_report(self):
85        pass
86
87
88    def assert_(self, expr, msg='Assertion failed.'):
89        if not expr:
90            raise error.TestError(msg)
91
92
93    def write_test_keyval(self, attr_dict):
94        utils.write_keyval(self.outputdir, attr_dict)
95
96
97    @staticmethod
98    def _append_type_to_keys(dictionary, typename):
99        new_dict = {}
100        for key, value in dictionary.iteritems():
101            new_key = "%s{%s}" % (key, typename)
102            new_dict[new_key] = value
103        return new_dict
104
105
106    def output_perf_value(self, description, value, units=None,
107                          higher_is_better=None, graph=None,
108                          replacement='_', replace_existing_values=False):
109        """
110        Records a measured performance value in an output file.
111
112        The output file will subsequently be parsed by the TKO parser to have
113        the information inserted into the results database.
114
115        @param description: A string describing the measured perf value. Must
116                be maximum length 256, and may only contain letters, numbers,
117                periods, dashes, and underscores.  For example:
118                "page_load_time", "scrolling-frame-rate".
119        @param value: A number representing the measured perf value, or a list
120                of measured values if a test takes multiple measurements.
121                Measured perf values can be either ints or floats.
122        @param units: A string describing the units associated with the
123                measured perf value. Must be maximum length 32, and may only
124                contain letters, numbers, periods, dashes, and underscores.
125                For example: "msec", "fps", "score", "runs_per_second".
126        @param higher_is_better: A boolean indicating whether or not a "higher"
127                measured perf value is considered to be better. If False, it is
128                assumed that a "lower" measured value is considered to be
129                better. This impacts dashboard plotting and email notification.
130                Pure autotests are expected to specify either True or False!
131                This value can be set to "None" to indicate that the perf
132                dashboard should apply the rules encoded via Chromium
133                unit-info.json. This is only used for tracking Chromium based
134                tests (in particular telemetry).
135        @param graph: A string indicating the name of the graph on which
136                the perf value will be subsequently displayed on the chrome perf
137                dashboard. This allows multiple metrics be grouped together on
138                the same graphs. Defaults to None, indicating that the perf
139                value should be displayed individually on a separate graph.
140        @param replacement: string to replace illegal characters in
141                |description| and |units| with.
142        @param replace_existing_values: A boolean indicating whether or not a
143                new added perf value should replace existing perf.
144        """
145        if len(description) > 256:
146            raise ValueError('The description must be at most 256 characters.')
147        if units and len(units) > 32:
148            raise ValueError('The units must be at most 32 characters.')
149
150        # If |replacement| is legal replace illegal characters with it.
151        string_regex = re.compile(r'[^-\.\w]')
152        if replacement is None or re.search(string_regex, replacement):
153            raise ValueError('Invalid replacement string to mask illegal '
154                             'characters. May only contain letters, numbers, '
155                             'periods, dashes, and underscores. '
156                             'replacement: %s' % replacement)
157        description = re.sub(string_regex, replacement, description)
158        units = re.sub(string_regex, replacement, units) if units else None
159
160        charts = {}
161        output_file = os.path.join(self.resultsdir, 'results-chart.json')
162        if os.path.isfile(output_file):
163            with open(output_file, 'r') as fp:
164                contents = fp.read()
165                if contents:
166                     charts = json.loads(contents)
167
168        if graph:
169            first_level = graph
170            second_level = description
171        else:
172            first_level = description
173            second_level = 'summary'
174
175        direction = 'up' if higher_is_better else 'down'
176
177        # All input should be a number - but at times there are strings
178        # representing numbers logged, attempt to convert them to numbers.
179        # If a non number string is logged an exception will be thrown.
180        if isinstance(value, list):
181          value = map(float, value)
182        else:
183          value = float(value)
184
185        result_type = 'scalar'
186        value_key = 'value'
187        result_value = value
188
189        # The chart json spec go/telemetry-json differenciates between a single
190        # value vs a list of values.  Lists of values get extra processing in
191        # the chromeperf dashboard ( mean, standard deviation etc)
192        # Tests can log one or more values for the same metric, to adhere stricly
193        # to the specification the first value logged is a scalar but if another
194        # value is logged the results become a list of scalar.
195        # TODO Figure out if there would be any difference of always using list
196        # of scalar even if there is just one item in the list.
197        if isinstance(value, list):
198            result_type = 'list_of_scalar_values'
199            value_key = 'values'
200            if first_level in charts and second_level in charts[first_level]:
201                if 'values' in charts[first_level][second_level]:
202                    result_value = charts[first_level][second_level]['values']
203                elif 'value' in charts[first_level][second_level]:
204                    result_value = [charts[first_level][second_level]['value']]
205                if replace_existing_values:
206                    result_value = value
207                else:
208                    result_value.extend(value)
209            else:
210                result_value = value
211        elif (first_level in charts and second_level in charts[first_level] and
212              not replace_existing_values):
213            result_type = 'list_of_scalar_values'
214            value_key = 'values'
215            if 'values' in charts[first_level][second_level]:
216                result_value = charts[first_level][second_level]['values']
217                result_value.append(value)
218            else:
219                result_value = [charts[first_level][second_level]['value'], value]
220
221        test_data = {
222            second_level: {
223                 'type': result_type,
224                 'units': units,
225                 value_key: result_value,
226                 'improvement_direction': direction
227           }
228        }
229
230        if first_level in charts:
231            charts[first_level].update(test_data)
232        else:
233            charts.update({first_level: test_data})
234
235        with open(output_file, 'w') as fp:
236            fp.write(json.dumps(charts, indent=2))
237
238
239    def write_perf_keyval(self, perf_dict):
240        self.write_iteration_keyval({}, perf_dict)
241
242
243    def write_attr_keyval(self, attr_dict):
244        self.write_iteration_keyval(attr_dict, {})
245
246
247    def write_iteration_keyval(self, attr_dict, perf_dict):
248        # append the dictionaries before they have the {perf} and {attr} added
249        self._keyvals.append({'attr':attr_dict, 'perf':perf_dict})
250        self._new_keyval = True
251
252        if attr_dict:
253            attr_dict = self._append_type_to_keys(attr_dict, "attr")
254            utils.write_keyval(self.resultsdir, attr_dict, type_tag="attr")
255
256        if perf_dict:
257            perf_dict = self._append_type_to_keys(perf_dict, "perf")
258            utils.write_keyval(self.resultsdir, perf_dict, type_tag="perf")
259
260        keyval_path = os.path.join(self.resultsdir, "keyval")
261        print >> open(keyval_path, "a"), ""
262
263
264    def analyze_perf_constraints(self, constraints):
265        if not self._new_keyval:
266            return
267
268        # create a dict from the keyvals suitable as an environment for eval
269        keyval_env = self._keyvals[-1]['perf'].copy()
270        keyval_env['__builtins__'] = None
271        self._new_keyval = False
272        failures = []
273
274        # evaluate each constraint using the current keyvals
275        for constraint in constraints:
276            logging.info('___________________ constraint = %s', constraint)
277            logging.info('___________________ keyvals = %s', keyval_env)
278
279            try:
280                if not eval(constraint, keyval_env):
281                    failures.append('%s: constraint was not met' % constraint)
282            except:
283                failures.append('could not evaluate constraint: %s'
284                                % constraint)
285
286        # keep track of the errors for each iteration
287        self.failed_constraints.append(failures)
288
289
290    def process_failed_constraints(self):
291        msg = ''
292        for i, failures in enumerate(self.failed_constraints):
293            if failures:
294                msg += 'iteration %d:%s  ' % (i, ','.join(failures))
295
296        if msg:
297            raise error.TestFail(msg)
298
299
300    def register_before_iteration_hook(self, iteration_hook):
301        """
302        This is how we expect test writers to register a before_iteration_hook.
303        This adds the method to the list of hooks which are executed
304        before each iteration.
305
306        @param iteration_hook: Method to run before each iteration. A valid
307                               hook accepts a single argument which is the
308                               test object.
309        """
310        self.before_iteration_hooks.append(iteration_hook)
311
312
313    def register_after_iteration_hook(self, iteration_hook):
314        """
315        This is how we expect test writers to register an after_iteration_hook.
316        This adds the method to the list of hooks which are executed
317        after each iteration. Hooks are executed starting with the most-
318        recently registered, in stack fashion.
319
320        @param iteration_hook: Method to run after each iteration. A valid
321                               hook accepts a single argument which is the
322                               test object.
323        """
324        self.after_iteration_hooks.append(iteration_hook)
325
326
327    def initialize(self):
328        pass
329
330
331    def setup(self):
332        pass
333
334
335    def warmup(self, *args, **dargs):
336        pass
337
338
339    def drop_caches_between_iterations(self):
340        if self.job.drop_caches_between_iterations:
341            utils.drop_caches()
342
343
344    def _call_run_once(self, constraints, profile_only,
345                       postprocess_profiled_run, args, dargs):
346        self.drop_caches_between_iterations()
347        # execute iteration hooks
348        if not self.job.fast:
349            logging.debug('Starting before_iteration_hooks for %s',
350                          self.tagged_testname)
351            with metrics.SecondsTimer(
352                    'chromeos/autotest/job/before_iteration_hook_duration'):
353                for hook in self.before_iteration_hooks:
354                    hook(self)
355            logging.debug('before_iteration_hooks completed')
356
357        finished = False
358        try:
359            if profile_only:
360                if not self.job.profilers.present():
361                    self.job.record('WARN', None, None,
362                                    'No profilers have been added but '
363                                    'profile_only is set - nothing '
364                                    'will be run')
365                self.run_once_profiling(postprocess_profiled_run,
366                                        *args, **dargs)
367            else:
368                self.before_run_once()
369                logging.debug('starting test(run_once()), test details follow'
370                              '\n%r', args)
371                self.run_once(*args, **dargs)
372                logging.debug('The test has completed successfully')
373                self.after_run_once()
374
375            self.postprocess_iteration()
376            self.analyze_perf_constraints(constraints)
377            finished = True
378        # Catch and re-raise to let after_iteration_hooks see the exception.
379        except Exception as e:
380            logging.debug('Test failed due to %s. Exception log follows the '
381                          'after_iteration_hooks.', str(e))
382            raise
383        finally:
384            if not finished or not self.job.fast:
385                logging.debug('Starting after_iteration_hooks for %s',
386                              self.tagged_testname)
387                with metrics.SecondsTimer(
388                        'chromeos/autotest/job/after_iteration_hook_duration'):
389                    for hook in reversed(self.after_iteration_hooks):
390                        hook(self)
391                logging.debug('after_iteration_hooks completed')
392
393
394    def execute(self, iterations=None, test_length=None, profile_only=None,
395                _get_time=time.time, postprocess_profiled_run=None,
396                constraints=(), *args, **dargs):
397        """
398        This is the basic execute method for the tests inherited from base_test.
399        If you want to implement a benchmark test, it's better to implement
400        the run_once function, to cope with the profiling infrastructure. For
401        other tests, you can just override the default implementation.
402
403        @param test_length: The minimum test length in seconds. We'll run the
404            run_once function for a number of times large enough to cover the
405            minimum test length.
406
407        @param iterations: A number of iterations that we'll run the run_once
408            function. This parameter is incompatible with test_length and will
409            be silently ignored if you specify both.
410
411        @param profile_only: If true run X iterations with profilers enabled.
412            If false run X iterations and one with profiling if profiles are
413            enabled. If None, default to the value of job.default_profile_only.
414
415        @param _get_time: [time.time] Used for unit test time injection.
416
417        @param postprocess_profiled_run: Run the postprocessing for the
418            profiled run.
419        """
420
421        # For our special class of tests, the benchmarks, we don't want
422        # profilers to run during the test iterations. Let's reserve only
423        # the last iteration for profiling, if needed. So let's stop
424        # all profilers if they are present and active.
425        profilers = self.job.profilers
426        if profilers.active():
427            profilers.stop(self)
428        if profile_only is None:
429            profile_only = self.job.default_profile_only
430        # If the user called this test in an odd way (specified both iterations
431        # and test_length), let's warn them.
432        if iterations and test_length:
433            logging.debug('Iterations parameter ignored (timed execution)')
434        if test_length:
435            test_start = _get_time()
436            time_elapsed = 0
437            timed_counter = 0
438            logging.debug('Test started. Specified %d s as the minimum test '
439                          'length', test_length)
440            while time_elapsed < test_length:
441                timed_counter = timed_counter + 1
442                if time_elapsed == 0:
443                    logging.debug('Executing iteration %d', timed_counter)
444                elif time_elapsed > 0:
445                    logging.debug('Executing iteration %d, time_elapsed %d s',
446                                  timed_counter, time_elapsed)
447                self._call_run_once(constraints, profile_only,
448                                    postprocess_profiled_run, args, dargs)
449                test_iteration_finish = _get_time()
450                time_elapsed = test_iteration_finish - test_start
451            logging.debug('Test finished after %d iterations, '
452                          'time elapsed: %d s', timed_counter, time_elapsed)
453        else:
454            if iterations is None:
455                iterations = 1
456            if iterations > 1:
457                logging.debug('Test started. Specified %d iterations',
458                              iterations)
459            for self.iteration in xrange(1, iterations + 1):
460                if iterations > 1:
461                    logging.debug('Executing iteration %d of %d',
462                                  self.iteration, iterations)
463                self._call_run_once(constraints, profile_only,
464                                    postprocess_profiled_run, args, dargs)
465
466        if not profile_only:
467            self.iteration += 1
468            self.run_once_profiling(postprocess_profiled_run, *args, **dargs)
469
470        # Do any postprocessing, normally extracting performance keyvals, etc
471        self.postprocess()
472        self.process_failed_constraints()
473
474
475    def run_once_profiling(self, postprocess_profiled_run, *args, **dargs):
476        profilers = self.job.profilers
477        # Do a profiling run if necessary
478        if profilers.present():
479            self.drop_caches_between_iterations()
480            profilers.before_start(self)
481
482            self.before_run_once()
483            profilers.start(self)
484            logging.debug('Profilers present. Profiling run started')
485
486            try:
487                self.run_once(*args, **dargs)
488
489                # Priority to the run_once() argument over the attribute.
490                postprocess_attribute = getattr(self,
491                                                'postprocess_profiled_run',
492                                                False)
493
494                if (postprocess_profiled_run or
495                    (postprocess_profiled_run is None and
496                     postprocess_attribute)):
497                    self.postprocess_iteration()
498
499            finally:
500                profilers.stop(self)
501                profilers.report(self)
502
503            self.after_run_once()
504
505
506    def postprocess(self):
507        pass
508
509
510    def postprocess_iteration(self):
511        pass
512
513
514    def cleanup(self):
515        pass
516
517
518    def before_run_once(self):
519        """
520        Override in tests that need it, will be called before any run_once()
521        call including the profiling run (when it's called before starting
522        the profilers).
523        """
524        pass
525
526
527    def after_run_once(self):
528        """
529        Called after every run_once (including from a profiled run when it's
530        called after stopping the profilers).
531        """
532        pass
533
534
535    @staticmethod
536    def _make_writable_to_others(directory):
537        mode = os.stat(directory).st_mode
538        mode = mode | stat.S_IROTH | stat.S_IWOTH | stat.S_IXOTH
539        os.chmod(directory, mode)
540
541
542    def _exec(self, args, dargs):
543        self.job.logging.tee_redirect_debug_dir(self.debugdir,
544                                                log_name=self.tagged_testname)
545        try:
546            # write out the test attributes into a keyval
547            dargs   = dargs.copy()
548            run_cleanup = dargs.pop('run_cleanup', self.job.run_test_cleanup)
549            keyvals = dargs.pop('test_attributes', {}).copy()
550            keyvals['version'] = self.version
551            for i, arg in enumerate(args):
552                keyvals['param-%d' % i] = repr(arg)
553            for name, arg in dargs.iteritems():
554                keyvals['param-%s' % name] = repr(arg)
555            self.write_test_keyval(keyvals)
556
557            _validate_args(args, dargs, self.initialize, self.setup,
558                           self.execute, self.cleanup)
559
560            try:
561                # Make resultsdir and tmpdir accessible to everyone. We may
562                # output data to these directories as others, e.g., chronos.
563                self._make_writable_to_others(self.tmpdir)
564                self._make_writable_to_others(self.resultsdir)
565
566                # Initialize:
567                _cherry_pick_call(self.initialize, *args, **dargs)
568
569                lockfile = open(os.path.join(self.job.tmpdir, '.testlock'), 'w')
570                try:
571                    fcntl.flock(lockfile, fcntl.LOCK_EX)
572                    # Setup: (compile and install the test, if needed)
573                    p_args, p_dargs = _cherry_pick_args(self.setup, args, dargs)
574                    utils.update_version(self.srcdir, self.preserve_srcdir,
575                                         self.version, self.setup,
576                                         *p_args, **p_dargs)
577                finally:
578                    fcntl.flock(lockfile, fcntl.LOCK_UN)
579                    lockfile.close()
580
581                # Execute:
582                os.chdir(self.outputdir)
583
584                # call self.warmup cherry picking the arguments it accepts and
585                # translate exceptions if needed
586                _call_test_function(_cherry_pick_call, self.warmup,
587                                    *args, **dargs)
588
589                if hasattr(self, 'run_once'):
590                    p_args, p_dargs = _cherry_pick_args(self.run_once,
591                                                        args, dargs)
592                    # pull in any non-* and non-** args from self.execute
593                    for param in _get_nonstar_args(self.execute):
594                        if param in dargs:
595                            p_dargs[param] = dargs[param]
596                else:
597                    p_args, p_dargs = _cherry_pick_args(self.execute,
598                                                        args, dargs)
599
600                _call_test_function(self.execute, *p_args, **p_dargs)
601            except Exception:
602                # Save the exception while we run our cleanup() before
603                # reraising it, but log it to so actual time of error is known.
604                exc_info = sys.exc_info()
605                logging.warning('The test failed with the following exception',
606                                exc_info=True)
607
608                try:
609                    try:
610                        if run_cleanup:
611                            logging.debug('Running cleanup for test.')
612                            _cherry_pick_call(self.cleanup, *args, **dargs)
613                    except Exception:
614                        logging.error('Ignoring exception during cleanup() '
615                                      'phase:')
616                        traceback.print_exc()
617                        logging.error('Now raising the earlier %s error',
618                                      exc_info[0])
619                    self.crash_handler_report()
620                finally:
621                    # Raise exception after running cleanup, reporting crash,
622                    # and restoring job's logging, even if the first two
623                    # actions fail.
624                    self.job.logging.restore()
625                    try:
626                        raise exc_info[0], exc_info[1], exc_info[2]
627                    finally:
628                        # http://docs.python.org/library/sys.html#sys.exc_info
629                        # Be nice and prevent a circular reference.
630                        del exc_info
631            else:
632                try:
633                    if run_cleanup:
634                        _cherry_pick_call(self.cleanup, *args, **dargs)
635                    self.crash_handler_report()
636                finally:
637                    self.job.logging.restore()
638        except error.AutotestError:
639            # Pass already-categorized errors on up.
640            raise
641        except Exception, e:
642            # Anything else is an ERROR in our own code, not execute().
643            raise error.UnhandledTestError(e)
644
645    def runsubtest(self, url, *args, **dargs):
646        """
647        Execute another autotest test from inside the current test's scope.
648
649        @param test: Parent test.
650        @param url: Url of new test.
651        @param tag: Tag added to test name.
652        @param args: Args for subtest.
653        @param dargs: Dictionary with args for subtest.
654        @iterations: Number of subtest iterations.
655        @profile_only: If true execute one profiled run.
656        """
657        dargs["profile_only"] = dargs.get("profile_only", False)
658        test_basepath = self.outputdir[len(self.job.resultdir + "/"):]
659        return self.job.run_test(url, master_testpath=test_basepath,
660                                 *args, **dargs)
661
662
663def _get_nonstar_args(func):
664    """Extract all the (normal) function parameter names.
665
666    Given a function, returns a tuple of parameter names, specifically
667    excluding the * and ** parameters, if the function accepts them.
668
669    @param func: A callable that we want to chose arguments for.
670
671    @return: A tuple of parameters accepted by the function.
672    """
673    return func.func_code.co_varnames[:func.func_code.co_argcount]
674
675
676def _cherry_pick_args(func, args, dargs):
677    """Sanitize positional and keyword arguments before calling a function.
678
679    Given a callable (func), an argument tuple and a dictionary of keyword
680    arguments, pick only those arguments which the function is prepared to
681    accept and return a new argument tuple and keyword argument dictionary.
682
683    Args:
684      func: A callable that we want to choose arguments for.
685      args: A tuple of positional arguments to consider passing to func.
686      dargs: A dictionary of keyword arguments to consider passing to func.
687    Returns:
688      A tuple of: (args tuple, keyword arguments dictionary)
689    """
690    # Cherry pick args:
691    if func.func_code.co_flags & 0x04:
692        # func accepts *args, so return the entire args.
693        p_args = args
694    else:
695        p_args = ()
696
697    # Cherry pick dargs:
698    if func.func_code.co_flags & 0x08:
699        # func accepts **dargs, so return the entire dargs.
700        p_dargs = dargs
701    else:
702        # Only return the keyword arguments that func accepts.
703        p_dargs = {}
704        for param in _get_nonstar_args(func):
705            if param in dargs:
706                p_dargs[param] = dargs[param]
707
708    return p_args, p_dargs
709
710
711def _cherry_pick_call(func, *args, **dargs):
712    """Cherry picks arguments from args/dargs based on what "func" accepts
713    and calls the function with the picked arguments."""
714    p_args, p_dargs = _cherry_pick_args(func, args, dargs)
715    return func(*p_args, **p_dargs)
716
717
718def _validate_args(args, dargs, *funcs):
719    """Verify that arguments are appropriate for at least one callable.
720
721    Given a list of callables as additional parameters, verify that
722    the proposed keyword arguments in dargs will each be accepted by at least
723    one of the callables.
724
725    NOTE: args is currently not supported and must be empty.
726
727    Args:
728      args: A tuple of proposed positional arguments.
729      dargs: A dictionary of proposed keyword arguments.
730      *funcs: Callables to be searched for acceptance of args and dargs.
731    Raises:
732      error.AutotestError: if an arg won't be accepted by any of *funcs.
733    """
734    all_co_flags = 0
735    all_varnames = ()
736    for func in funcs:
737        all_co_flags |= func.func_code.co_flags
738        all_varnames += func.func_code.co_varnames[:func.func_code.co_argcount]
739
740    # Check if given args belongs to at least one of the methods below.
741    if len(args) > 0:
742        # Current implementation doesn't allow the use of args.
743        raise error.TestError('Unnamed arguments not accepted. Please '
744                              'call job.run_test with named args only')
745
746    # Check if given dargs belongs to at least one of the methods below.
747    if len(dargs) > 0:
748        if not all_co_flags & 0x08:
749            # no func accepts *dargs, so:
750            for param in dargs:
751                if not param in all_varnames:
752                    raise error.AutotestError('Unknown parameter: %s' % param)
753
754
755def _installtest(job, url):
756    (group, name) = job.pkgmgr.get_package_name(url, 'test')
757
758    # Bail if the test is already installed
759    group_dir = os.path.join(job.testdir, "download", group)
760    if os.path.exists(os.path.join(group_dir, name)):
761        return (group, name)
762
763    # If the group directory is missing create it and add
764    # an empty  __init__.py so that sub-directories are
765    # considered for import.
766    if not os.path.exists(group_dir):
767        os.makedirs(group_dir)
768        f = file(os.path.join(group_dir, '__init__.py'), 'w+')
769        f.close()
770
771    logging.debug("%s: installing test url=%s", name, url)
772    tarball = os.path.basename(url)
773    tarball_path = os.path.join(group_dir, tarball)
774    test_dir = os.path.join(group_dir, name)
775    job.pkgmgr.fetch_pkg(tarball, tarball_path,
776                         repo_url = os.path.dirname(url))
777
778    # Create the directory for the test
779    if not os.path.exists(test_dir):
780        os.mkdir(os.path.join(group_dir, name))
781
782    job.pkgmgr.untar_pkg(tarball_path, test_dir)
783
784    os.remove(tarball_path)
785
786    # For this 'sub-object' to be importable via the name
787    # 'group.name' we need to provide an __init__.py,
788    # so link the main entry point to this.
789    os.symlink(name + '.py', os.path.join(group_dir, name,
790                            '__init__.py'))
791
792    # The test is now installed.
793    return (group, name)
794
795
796def _call_test_function(func, *args, **dargs):
797    """Calls a test function and translates exceptions so that errors
798    inside test code are considered test failures."""
799    try:
800        return func(*args, **dargs)
801    except error.AutotestError:
802        raise
803    except Exception, e:
804        # Other exceptions must be treated as a FAIL when
805        # raised during the test functions
806        raise error.UnhandledTestFail(e)
807
808
809def runtest(job, url, tag, args, dargs,
810            local_namespace={}, global_namespace={},
811            before_test_hook=None, after_test_hook=None,
812            before_iteration_hook=None, after_iteration_hook=None):
813    local_namespace = local_namespace.copy()
814    global_namespace = global_namespace.copy()
815    # if this is not a plain test name then download and install the
816    # specified test
817    if url.endswith('.tar.bz2'):
818        (testgroup, testname) = _installtest(job, url)
819        bindir = os.path.join(job.testdir, 'download', testgroup, testname)
820        importdir = os.path.join(job.testdir, 'download')
821        modulename = '%s.%s' % (re.sub('/', '.', testgroup), testname)
822        classname = '%s.%s' % (modulename, testname)
823        path = testname
824    else:
825        # If the test is local, it may be under either testdir or site_testdir.
826        # Tests in site_testdir override tests defined in testdir
827        testname = path = url
828        testgroup = ''
829        path = re.sub(':', '/', testname)
830        modulename = os.path.basename(path)
831        classname = '%s.%s' % (modulename, modulename)
832
833        # Try installing the test package
834        # The job object may be either a server side job or a client side job.
835        # 'install_pkg' method will be present only if it's a client side job.
836        if hasattr(job, 'install_pkg'):
837            try:
838                bindir = os.path.join(job.testdir, testname)
839                job.install_pkg(testname, 'test', bindir)
840            except error.PackageInstallError:
841                # continue as a fall back mechanism and see if the test code
842                # already exists on the machine
843                pass
844
845        bindir = None
846        for dir in [job.testdir, getattr(job, 'site_testdir', None)]:
847            if dir is not None and os.path.exists(os.path.join(dir, path)):
848                importdir = bindir = os.path.join(dir, path)
849        if not bindir:
850            raise error.TestError(testname + ': test does not exist')
851
852    subdir = os.path.join(dargs.pop('master_testpath', ""), testname)
853    outputdir = os.path.join(job.resultdir, subdir)
854    if tag:
855        outputdir += '.' + tag
856
857    local_namespace['job'] = job
858    local_namespace['bindir'] = bindir
859    local_namespace['outputdir'] = outputdir
860
861    sys.path.insert(0, importdir)
862    try:
863        exec ('import %s' % modulename, local_namespace, global_namespace)
864        exec ("mytest = %s(job, bindir, outputdir)" % classname,
865              local_namespace, global_namespace)
866    finally:
867        sys.path.pop(0)
868
869    pwd = os.getcwd()
870    os.chdir(outputdir)
871
872    try:
873        mytest = global_namespace['mytest']
874        mytest.success = False
875        if not job.fast and before_test_hook:
876            logging.info('Starting before_hook for %s', mytest.tagged_testname)
877            with metrics.SecondsTimer(
878                    'chromeos/autotest/job/before_hook_duration'):
879                before_test_hook(mytest)
880            logging.info('before_hook completed')
881
882        # we use the register iteration hooks methods to register the passed
883        # in hooks
884        if before_iteration_hook:
885            mytest.register_before_iteration_hook(before_iteration_hook)
886        if after_iteration_hook:
887            mytest.register_after_iteration_hook(after_iteration_hook)
888        mytest._exec(args, dargs)
889        mytest.success = True
890    finally:
891        os.chdir(pwd)
892        if after_test_hook and (not mytest.success or not job.fast):
893            logging.info('Starting after_hook for %s', mytest.tagged_testname)
894            with metrics.SecondsTimer(
895                    'chromeos/autotest/job/after_hook_duration'):
896                after_test_hook(mytest)
897            logging.info('after_hook completed')
898
899        shutil.rmtree(mytest.tmpdir, ignore_errors=True)
900