=== modified file 'doc/external_measurement.rst'
@@ -68,6 +68,9 @@
should not be possible to crash the whole dispatcher with a typo in
one of them.
+There are other methods you might want to override in some situations
+-- see the source for more.
+
Here is a very simple complete handler::
import datetime
@@ -106,5 +109,107 @@
.. _`entry point`: http://packages.python.org/distribute/pkg_resources.html#entry-points
-We will soon provide a way to bundle the signal handler along with the
-test definition.
+Providing handlers as shell scripts
+-----------------------------------
+
+Using the 'shell-hooks' handler that is distributed with the
+dispatcher it is possible to write handlers as scripts in the same VCS
+repository as the test definition itself.
+
+The simplest usage looks like this::
+
+ handler:
+ handler-name: shell-hooks
+ params:
+ handlers:
+ start_testcase: start-testcase.sh
+ end_testcase: end-testcase.sh
+ postprocess_test_result: postprocess-test-result.sh
+
+The scripts named in ``handlers`` are invoked with a test-case
+specific directory as the current working directory so they can store
+and access data in local paths. The scripts named by
+``start_testcase`` and ``end_testcase`` are invoked with no arguments
+but ``postprocess_test_result`` is invoked with a single argument: a
+directory which contains the on-disk representation of the test result
+as produced on the device (this on-disk representation is not yet
+fully documented). If a hook produces output, it will be attached to
+the test result.
+
+As many interesting hooks need to have information about the device
+being tested, there is a facility for putting values from the device
+config into the environment of the hooks. For example, the following
+test definition sets the environment variable ``$DEVICE_TYPE`` to the
+value of the ``device_type`` key::
+
+ handler:
+ handler-name: shell-hooks
+ params:
+ device_config_vars:
+ DEVICE_TYPE: device_type
+ handlers:
+ ...
+
+For a slightly silly example of a shell hook, let's try to mark any
+test that takes more than 10 seconds (as viewed from the host) as
+failed, even if they report success on the device, and also attach
+some meta data about the device to each test result.
+
+The start hook (``start-hook.sh`` in the repository) just records the
+current unix timestamp in a file (we can just use the cwd as a scratch
+storage area)::
+
+ #!/bin/sh
+ date +%s > start-time
+
+The end hook (``end-hook.sh``) just records the end time::
+
+ #!/bin/sh
+ date +%s > end-time
+
+The postprocess hook (``post-process-result-hook.sh``) reads the times
+recorded by the above hooks, overwrites the result if necessary and
+creates an attachment containing the device type::
+
+ #!/bin/sh
+ start_time=`cat start-time`
+ end_time=`cat end-time`
+ if [ $((end_time - start_time)) -gt 10 ]; then
+ echo fail > $1/result
+ fi
+ echo $DEVICE_TYPE > $1/attachments/device-type.txt
+
+A test definition that glues this all together would be::
+
+ metadata:
+ format: Lava-Test Test Definition 1.0
+ name: shell-hook-example
+
+ run:
+ steps:
+ - lava-test-case pass-test --shell sleep 5
+ - lava-test-case fail-test --shell sleep 15
+
+ handler:
+ handler-name: shell-hooks
+ params:
+ device_config_vars:
+ DEVICE_TYPE: device_type
+ handlers:
+ start_testcase: start-hook.sh
+ end_testcase: end-hook.sh
+ postprocess_test_result: post-process-result-hook.sh
+
+A repository with all the above piece is on Launchpad in the branch
+`lp:~linaro-validation/+junk/shell-hook-example`_ so an action for
+your job file might look like::
+
+ {
+ "command": "lava_test_shell",
+ "parameters": {
+ "testdef_repos": [{"bzr-repo": "lp:~linaro-validation/+junk/shell-hook-example"}],
+ "timeout": 1800
+ }
+ },
+
+.. _`lp:~linaro-validation/+junk/shell-hook-example`: http://bazaar.launchpad.net/~linaro-validation/+junk/shell-hook-example/files
=== modified file 'lava_dispatcher/lava_test_shell.py'
@@ -25,6 +25,7 @@
by a lava-test-shell run.
"""
+import base64
import datetime
import decimal
import mimetypes
@@ -143,6 +144,31 @@
return attributes
+def _result_to_dir(test_result, dir):
+
+ def w(name, content):
+ with open(os.path.join(dir, name), 'w') as f:
+ f.write(str(content) + '\n')
+
+ for name in 'result', 'measurement', 'units', 'message', 'timestamp', 'duration':
+ if name in test_result:
+ w(name, test_result[name])
+
+
+ os.makedirs(os.path.join(dir, 'attachments'))
+
+ for attachment in test_result.get('attachments', []):
+ path = 'attachments/' + attachment['pathname']
+ w(path, base64.b64decode(attachment['content']))
+ w(path + '.mimetype', attachment['mime_type'])
+
+ os.makedirs(os.path.join(dir, 'attributes'))
+
+ for attrname, attrvalue in test_result.get('attributes', []).items():
+ path = 'attributes/' + attrname
+ w(path, attrvalue)
+
+
def _result_from_dir(dir):
result = {
'test_case_id': os.path.basename(dir),
=== modified file 'lava_dispatcher/signals/__init__.py'
@@ -1,5 +1,3 @@
-#!/usr/bin/python
-
# Copyright (C) 2012 Linaro Limited
#
# Author: Andy Doan <andy.doan@linaro.org>
=== added file 'lava_dispatcher/signals/shellhooks.py'
@@ -0,0 +1,87 @@
+from ConfigParser import NoOptionError
+import logging
+import shutil
+import subprocess
+import os
+import tempfile
+
+from lava_dispatcher.lava_test_shell import (
+ _read_content,
+ _result_to_dir,
+ _result_from_dir)
+from lava_dispatcher.signals import SignalHandler
+from lava_dispatcher.test_data import create_attachment
+from lava_dispatcher.utils import mkdtemp
+
+class ShellHooks(SignalHandler):
+
+ def __init__(self, testdef_obj, handlers={}, device_config_vars={}):
+ SignalHandler.__init__(self, testdef_obj)
+ self.result_dir = mkdtemp()
+ self.handlers = handlers
+ self.scratch_dir = mkdtemp()
+ self.code_dir = os.path.join(self.scratch_dir, 'code')
+ shutil.copytree(testdef_obj.repo, self.code_dir)
+ device_config = testdef_obj.context.client.target_device.config
+ self.our_env = os.environ.copy()
+ for env_var, config_var in device_config_vars.iteritems():
+ try:
+ config_value = device_config.cp.get('__main__', config_var)
+ except NoOptionError:
+ logging.warning(
+ "No value found for device config %s; leaving %s unset "
+ "in environment", config_var, env_var)
+ else:
+ self.our_env[env_var] = config_value
+
+ def _invoke_hook(self, name, working_dir, args=[]):
+ script_name = self.handlers.get(name)
+ if not script_name:
+ return
+ script = os.path.join(self.code_dir, script_name)
+ if not os.path.exists(script):
+ logging.warning("handler script %s not found", script_name)
+ return
+ (fd, path) = tempfile.mkstemp(dir=self.code_dir)
+ status = subprocess.call(
+ [script] + args, cwd=working_dir, env=self.our_env,
+ stdout=fd, stderr=subprocess.STDOUT)
+ if status != 0:
+ logging.warning(
+ "%s handler script exited with code %s", name, status)
+ return path
+
+ def start_testcase(self, test_case_id):
+ case_dir = os.path.join(self.result_dir, test_case_id)
+ os.mkdir(case_dir)
+ case_data = {'case_dir': case_dir}
+ case_data['start_testcase_output'] = self._invoke_hook(
+ 'start_testcase', case_dir)
+ return case_data
+
+ def end_testcase(self, test_case_id, case_data):
+ case_data['end_testcase_output'] = self._invoke_hook(
+ 'end_testcase', case_data['case_dir'])
+
+ def postprocess_test_result(self, test_result, case_data):
+ test_case_id = test_result['test_case_id']
+ scratch_dir = tempfile.mkdtemp()
+ try:
+ result_dir = os.path.join(scratch_dir, test_case_id)
+ os.mkdir(result_dir)
+ _result_to_dir(test_result, result_dir)
+ case_data['postprocess_test_result_output'] = self._invoke_hook(
+ 'postprocess_test_result', case_data['case_dir'], [result_dir])
+ test_result.clear()
+ test_result.update(_result_from_dir(result_dir))
+ finally:
+ shutil.rmtree(scratch_dir)
+ for key in 'start_testcase_output', 'end_testcase_output', \
+ 'postprocess_test_result_output':
+ path = case_data.get(key)
+ if path is None:
+ continue
+ content = _read_content(path, ignore_missing=True)
+ if content:
+ test_result['attachments'].append(
+ create_attachment(key + '.txt', _read_content(path)))
=== modified file 'setup.py'
@@ -17,8 +17,10 @@
connect = lava.dispatcher.commands:connect
devices = lava.dispatcher.commands:devices
power-cycle = lava.dispatcher.commands:power_cycle
+
[lava.signal_handlers]
add-duration = lava_dispatcher.signals.duration:AddDuration
+ shell-hooks = lava_dispatcher.signals.shellhooks:ShellHooks
""",
packages=find_packages(),
package_data= {