# SPDX-License-Identifier: Apache-2.0 # # Copyright (C) 2015, ARM Limited and contributors. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # import os import unittest import logging from bart.sched.SchedAssert import SchedAssert from bart.sched.SchedMultiAssert import SchedMultiAssert from devlib.utils.misc import memoized import wrapt from env import TestEnv from executor import Executor from trace import Trace class LisaTest(unittest.TestCase): """ A base class for LISA tests This class is intended to be subclassed in order to create automated tests for LISA. It sets up the TestEnv and Executor and provides convenience methods for making assertions on results. Subclasses should provide a test_conf to configure the TestEnv and an experiments_conf to configure the executor. Tests whose behaviour is dependent on target parameters, for example presence of cpufreq governors or number of CPUs, can override _getExperimentsConf to generate target-dependent experiments. Example users of this class can be found under LISA's tests/ directory. :ivar experiments: List of :class:`Experiment` s executed for the test. Only available after :meth:`init` has been called. """ test_conf = None """Override this with a dictionary or JSON path to configure the TestEnv""" experiments_conf = None """Override this with a dictionary or JSON path to configure the Executor""" permitted_fail_pct = 0 """The percentage of iterations of each test that may be permitted to fail""" @classmethod def _getTestConf(cls): if cls.test_conf is None: raise NotImplementedError("Override `test_conf` attribute") return cls.test_conf @classmethod def _getExperimentsConf(cls, test_env): """ Get the experiments_conf used to configure the Executor This method receives the initialized TestEnv as a parameter, so subclasses can override it to configure workloads or target confs in a manner dependent on the target. If not overridden, just returns the experiments_conf attribute. """ if cls.experiments_conf is None: raise NotImplementedError("Override `experiments_conf` attribute") return cls.experiments_conf @classmethod def runExperiments(cls): """ Set up logging and trigger running experiments """ cls._log = logging.getLogger('LisaTest') cls._log.info('Setup tests execution engine...') test_env = TestEnv(test_conf=cls._getTestConf()) experiments_conf = cls._getExperimentsConf(test_env) if ITERATIONS_FROM_CMDLINE: if 'iterations' in experiments_conf: cls.logger.warning( "Command line overrides iteration count in " "{}'s experiments_conf".format(cls.__name__)) experiments_conf['iterations'] = ITERATIONS_FROM_CMDLINE cls.executor = Executor(test_env, experiments_conf) # Alias tests and workloads configurations cls.wloads = cls.executor._experiments_conf["wloads"] cls.confs = cls.executor._experiments_conf["confs"] # Alias executor objects to make less verbose tests code cls.te = cls.executor.te cls.target = cls.executor.target # Execute pre-experiments code defined by the test cls._experimentsInit() cls._log.info('Experiments execution...') cls.executor.run() cls.experiments = cls.executor.experiments # Execute post-experiments code defined by the test cls._experimentsFinalize() @classmethod def _experimentsInit(cls): """ Code executed before running the experiments """ @classmethod def _experimentsFinalize(cls): """ Code executed after running the experiments """ @memoized def get_sched_assert(self, experiment, task): """ Return a SchedAssert over the task provided """ return SchedAssert( self.get_trace(experiment).ftrace, self.te.topology, execname=task) @memoized def get_multi_assert(self, experiment, task_filter=""): """ Return a SchedMultiAssert over the tasks whose names contain task_filter By default, this includes _all_ the tasks that were executed for the experiment. """ tasks = experiment.wload.tasks.keys() return SchedMultiAssert(self.get_trace(experiment).ftrace, self.te.topology, [t for t in tasks if task_filter in t]) def get_trace(self, experiment): if not hasattr(self, "__traces"): self.__traces = {} if experiment.out_dir in self.__traces: return self.__traces[experiment.out_dir] if ('ftrace' not in experiment.conf['flags'] or 'ftrace' not in self.test_conf): raise ValueError( 'Tracing not enabled. If this test needs a trace, add "ftrace" ' 'to your test/experiment configuration flags') events = self.test_conf['ftrace']['events'] trace = Trace(self.te.platform, experiment.out_dir, events) self.__traces[experiment.out_dir] = trace return trace def get_start_time(self, experiment): """ Get the time at which the experiment workload began executing """ start_times_dict = self.get_multi_assert(experiment).getStartTime() return min([t["starttime"] for t in start_times_dict.itervalues()]) def get_end_time(self, experiment): """ Get the time at which the experiment workload finished executing """ end_times_dict = self.get_multi_assert(experiment).getEndTime() return max([t["endtime"] for t in end_times_dict.itervalues()]) def get_window(self, experiment): return (self.get_start_time(experiment), self.get_end_time(experiment)) def get_end_times(self, experiment): """ Get the time at which each task in the workload finished Returned as a dict; {"task_name": finish_time, ...} """ end_times = {} ftrace = self.get_trace(experiment).ftrace for task in experiment.wload.tasks.keys(): sched_assert = SchedAssert(ftrace, self.te.topology, execname=task) end_times[task] = sched_assert.getEndTime() return end_times def _dummy_method(self): pass # In the Python unittest framework you instantiate TestCase objects passing # the name of a test method that is going to be run to make assertions. We # run our tests using nosetests, which automatically discovers these # methods. However we also want to be able to instantiate LisaTest objects # in notebooks without the inconvenience of having to provide a methodName, # since we won't need any assertions. So we'll override __init__ with a # default dummy test method that does nothing. def __init__(self, methodName='_dummy_method', *args, **kwargs): super(LisaTest, self).__init__(methodName, *args, **kwargs) @wrapt.decorator def experiment_test(wrapped_test, instance, args, kwargs): """ Convert a LisaTest test method to be automatically called for each experiment The method will be passed the experiment object and a list of the names of tasks that were run as the experiment's workload. """ failures = {} for experiment in instance.executor.experiments: tasks = experiment.wload.tasks.keys() try: wrapped_test(experiment, tasks, *args, **kwargs) except AssertionError as e: trace_relpath = os.path.join(experiment.out_dir, "trace.dat") add_msg = "Check trace file: " + os.path.abspath(trace_relpath) msg = str(e) + "\n\t" + add_msg test_key = (experiment.wload_name, experiment.conf['tag']) failures[test_key] = failures.get(test_key, []) + [msg] for fails in failures.itervalues(): iterations = instance.executor.iterations fail_pct = 100. * len(fails) / iterations msg = "{} failures from {} iteration(s):\n{}".format( len(fails), iterations, '\n'.join(fails)) if fail_pct > instance.permitted_fail_pct: raise AssertionError(msg) else: instance._log.warning(msg) instance._log.warning( 'ALLOWING due to permitted_fail_pct={}'.format( instance.permitted_fail_pct)) # Prevent nosetests from running experiment_test directly as a test case experiment_test.__test__ = False # Allow the user to override the iterations setting from the command # line. Nosetests does not support this kind of thing, so we use an # evil hack: the lisa-test shell function takes an --iterations # argument and exports an environment variable. If the test itself # specifies an iterations count, we'll later print a warning and # override it. We do this here in the root scope, rather than in # runExperiments, so that if the value is invalid we print the error # immediately instead of going ahead with target setup etc. try: ITERATIONS_FROM_CMDLINE = int( os.getenv('LISA_TEST_ITERATIONS', '0')) if ITERATIONS_FROM_CMDLINE < 0: raise ValueError('Cannot be negative') except ValueError as e: raise ValueError("Couldn't read iterations count: {}".format(e)) # vim :set tabstop=4 shiftwidth=4 expandtab