diff --git a/wlauto/core/execution.py b/wlauto/core/execution.py index e0519385..6f5d981f 100644 --- a/wlauto/core/execution.py +++ b/wlauto/core/execution.py @@ -527,6 +527,10 @@ class Runner(object): self.logger.info('Initializing device') self.device.initialize(self.context) + self.logger.info('Initializing workloads') + for workload_spec in self.context.config.workload_specs: + workload_spec.workload.initialize(self.context) + props = self.device.get_properties(self.context) self.context.run_info.device_properties = props self.result_manager.initialize(self.context) diff --git a/wlauto/core/extension.py b/wlauto/core/extension.py index af7aa93e..037f2b34 100644 --- a/wlauto/core/extension.py +++ b/wlauto/core/extension.py @@ -392,7 +392,8 @@ class ExtensionMeta(type): ('core_modules', str, ListCollection), ] - virtual_methods = ['validate'] + virtual_methods = ['validate', 'initialize', 'finalize'] + global_virtuals = ['initialize', 'finalize'] def __new__(mcs, clsname, bases, attrs): mcs._propagate_attributes(bases, attrs) @@ -441,13 +442,13 @@ class ExtensionMeta(type): super(cls, self).vmname() - .. note:: current implementation imposes a restriction in that - parameters into the function *must* be passed as keyword - arguments. There *must not* be positional arguments on - virutal method invocation. + This also ensures that the methods that have beend identified as + "globally virtual" are executed exactly once per WA execution, even if + invoked through instances of different subclasses """ methods = {} + called_globals = set() for vmname in mcs.virtual_methods: clsmethod = getattr(cls, vmname, None) if clsmethod: @@ -455,11 +456,24 @@ class ExtensionMeta(type): methods[vmname] = [bm for bm in basemethods if bm != clsmethod] methods[vmname].append(clsmethod) - def wrapper(self, __name=vmname, **kwargs): - for dm in methods[__name]: - dm(self, **kwargs) + def generate_method_wrapper(vname): # pylint: disable=unused-argument + # this creates a closure with the method name so that it + # does not need to be passed to the wrapper as an argument, + # leaving the wrapper to accept exactly the same set of + # arguments as the method it is wrapping. + name__ = vmname # pylint: disable=cell-var-from-loop - setattr(cls, vmname, wrapper) + def wrapper(self, *args, **kwargs): + for dm in methods[name__]: + if name__ in mcs.global_virtuals: + if dm not in called_globals: + dm(self, *args, **kwargs) + called_globals.add(dm) + else: + dm(self, *args, **kwargs) + return wrapper + + setattr(cls, vmname, generate_method_wrapper(vmname)) class Extension(object): @@ -539,6 +553,12 @@ class Extension(object): for param in self.parameters: param.validate(self) + def initialize(self, *args, **kwargs): + pass + + def finalize(self, *args, **kwargs): + pass + def check_artifacts(self, context, level): """ Make sure that all mandatory artifacts have been generated. diff --git a/wlauto/core/instrumentation.py b/wlauto/core/instrumentation.py index b4dbfa95..4d91c585 100644 --- a/wlauto/core/instrumentation.py +++ b/wlauto/core/instrumentation.py @@ -288,9 +288,15 @@ def install(instrument): attr = getattr(instrument, attr_name) if not callable(attr): raise ValueError('Attribute {} not callable in {}.'.format(attr_name, instrument)) - arg_num = len(inspect.getargspec(attr).args) - if not arg_num == 2: - raise ValueError('{} must take exactly 2 arguments; {} given.'.format(attr_name, arg_num)) + argspec = inspect.getargspec(attr) + arg_num = len(argspec.args) + # Instrument callbacks will be passed exactly two arguments: self + # (the instrument instance to which the callback is bound) and + # context. However, we also allow callbacks to capture the context + # in variable arguments (declared as "*args" in the definition). + if arg_num > 2 or (arg_num < 2 and argspec.varargs is None): + message = '{} must take exactly positional arguments; {} given.' + raise ValueError(message.format(attr_name, arg_num)) logger.debug('\tConnecting %s to %s', attr.__name__, SIGNAL_MAP[stripped_attr_name]) mc = ManagedCallback(instrument, attr) @@ -379,6 +385,12 @@ class Instrument(Extension): self.is_enabled = True self.is_broken = False + def initialize(self, context): # pylint: disable=arguments-differ + pass + + def finalize(self, context): # pylint: disable=arguments-differ + pass + def __str__(self): return self.name diff --git a/wlauto/core/result.py b/wlauto/core/result.py index bacb228c..e290186f 100644 --- a/wlauto/core/result.py +++ b/wlauto/core/result.py @@ -140,7 +140,7 @@ class ResultProcessor(Extension): """ - def initialize(self, context): + def initialize(self, context): # pylint: disable=arguments-differ pass def process_iteration_result(self, result, context): @@ -155,7 +155,7 @@ class ResultProcessor(Extension): def export_run_result(self, result, context): pass - def finalize(self, context): + def finalize(self, context): # pylint: disable=arguments-differ pass diff --git a/wlauto/core/workload.py b/wlauto/core/workload.py index dad52aaa..fa61a545 100644 --- a/wlauto/core/workload.py +++ b/wlauto/core/workload.py @@ -53,18 +53,25 @@ class Workload(Extension): def init_resources(self, context): """ - May be optionally overridden by concrete instances in order to discover and initialise - necessary resources. This method will be invoked at most once during the execution: - before running any workloads, and before invocation of ``validate()``, but after it is - clear that this workload will run (i.e. this method will not be invoked for workloads - that have been discovered but have not been scheduled run in the agenda). + This method may be used to perform early resource discovery and initialization. This is invoked + during the initial loading stage and before the device is ready, so cannot be used for any + device-dependent initialization. This method is invoked before the workload instance is + validated. + + """ + pass + + def initialize(self, context): # pylint: disable=arguments-differ + """ + This method should be used to perform once-per-run initialization of a workload instance, i.e., + unlike ``setup()`` it will not be invoked on each iteration. """ pass def setup(self, context): """ - Perform the setup necessary to run the workload, such as copying the necessry files + Perform the setup necessary to run the workload, such as copying the necessary files to the device, configuring the environments, etc. This is also the place to perform any on-device checks prior to attempting to execute @@ -89,6 +96,9 @@ class Workload(Extension): """ Perform any final clean up for the Workload. """ pass + def finalize(self, context): # pylint: disable=arguments-differ + pass + def __str__(self): return ''.format(self.name) diff --git a/wlauto/tests/test_execution.py b/wlauto/tests/test_execution.py index fb18f24b..92259352 100644 --- a/wlauto/tests/test_execution.py +++ b/wlauto/tests/test_execution.py @@ -27,7 +27,7 @@ from wlauto.core.execution import BySpecRunner, ByIterationRunner from wlauto.exceptions import DeviceError from wlauto.core.configuration import WorkloadRunSpec, RebootPolicy from wlauto.core.instrumentation import Instrument -from wlauto.core.device import Device +from wlauto.core.device import Device, DeviceMeta from wlauto.core import instrumentation, signal from wlauto.core.workload import Workload from wlauto.core.result import IterationResult @@ -61,8 +61,37 @@ class Mock(object): pass +class BadDeviceMeta(DeviceMeta): + + @classmethod + def _implement_virtual(mcs, cls, bases): + """ + This version of _implement_virtual does not inforce "call global virutals only once" + policy, so that intialize() and finalize() my be invoked multiple times to test that + the errors they generated are handled correctly. + + """ + # pylint: disable=cell-var-from-loop,unused-argument + methods = {} + for vmname in mcs.virtual_methods: + clsmethod = getattr(cls, vmname, None) + if clsmethod: + basemethods = [getattr(b, vmname) for b in bases if hasattr(b, vmname)] + methods[vmname] = [bm for bm in basemethods if bm != clsmethod] + methods[vmname].append(clsmethod) + def generate_method_wrapper(vname): + name__ = vmname + def wrapper(self, *args, **kwargs): + for dm in methods[name__]: + dm(self, *args, **kwargs) + return wrapper + setattr(cls, vmname, generate_method_wrapper(vmname)) + + class BadDevice(Device): + __metaclass__ = BadDeviceMeta + def __init__(self, when_to_fail, exception=DeviceError): #pylint: disable=super-init-not-called self.when_to_fail = when_to_fail diff --git a/wlauto/tests/test_extension.py b/wlauto/tests/test_extension.py index 41794f93..cfa51cef 100644 --- a/wlauto/tests/test_extension.py +++ b/wlauto/tests/test_extension.py @@ -220,6 +220,63 @@ class ExtensionMetaTest(TestCase): assert_equal(acid.v2, 2) assert_equal(acid.vv2, 2) + def test_initialization(self): + class MyExt(Extension): + name = 'myext' + values = {'a': 0} + def __init__(self, *args, **kwargs): + super(MyExt, self).__init__(*args, **kwargs) + self.instance_init = 0 + def initialize(self): + self.values['a'] += 1 + + class MyChildExt(MyExt): + name = 'mychildext' + def initialize(self): + self.instance_init += 1 + + ext = _instantiate(MyChildExt) + ext.initialize() + + assert_equal(MyExt.values['a'], 1) + assert_equal(ext.instance_init, 1) + + def test_initialization_happens_once(self): + class MyExt(Extension): + name = 'myext' + values = {'a': 0} + def __init__(self, *args, **kwargs): + super(MyExt, self).__init__(*args, **kwargs) + self.instance_init = 0 + self.instance_validate = 0 + def initialize(self): + self.values['a'] += 1 + def validate(self): + self.instance_validate += 1 + + class MyChildExt(MyExt): + name = 'mychildext' + def initialize(self): + self.instance_init += 1 + def validate(self): + self.instance_validate += 1 + + ext1 = _instantiate(MyExt) + ext2 = _instantiate(MyExt) + ext3 = _instantiate(MyChildExt) + ext1.initialize() + ext2.initialize() + ext3.initialize() + ext1.validate() + ext2.validate() + ext3.validate() + + assert_equal(MyExt.values['a'], 1) + assert_equal(ext1.instance_init, 0) + assert_equal(ext3.instance_init, 1) + assert_equal(ext1.instance_validate, 1) + assert_equal(ext3.instance_validate, 2) + class ParametersTest(TestCase):