aboutsummaryrefslogtreecommitdiff
path: root/lava
diff options
context:
space:
mode:
Diffstat (limited to 'lava')
-rw-r--r--lava/commands.py227
-rw-r--r--lava/config.py294
-rw-r--r--lava/device/__init__.py97
-rw-r--r--lava/device/commands.py122
-rw-r--r--lava/device/templates.py82
-rw-r--r--lava/device/tests/__init__.py0
-rw-r--r--lava/device/tests/test_commands.py182
-rw-r--r--lava/device/tests/test_device.py119
-rw-r--r--lava/helper/__init__.py0
-rw-r--r--lava/helper/command.py244
-rw-r--r--lava/helper/dispatcher.py110
-rw-r--r--lava/helper/template.py124
-rw-r--r--lava/helper/tests/__init__.py0
-rw-r--r--lava/helper/tests/helper_test.py81
-rw-r--r--lava/helper/tests/test_command.py47
-rw-r--r--lava/helper/tests/test_dispatcher.py77
-rw-r--r--lava/helper/tests/test_template.py102
-rw-r--r--lava/job/__init__.py73
-rw-r--r--lava/job/commands.py107
-rw-r--r--lava/job/templates.py106
-rw-r--r--lava/job/tests/__init__.py0
-rw-r--r--lava/job/tests/test_commands.py155
-rw-r--r--lava/job/tests/test_job.py92
-rw-r--r--lava/parameter.py256
-rw-r--r--lava/script/__init__.py51
-rw-r--r--lava/script/commands.py115
-rw-r--r--lava/script/tests/__init__.py0
-rw-r--r--lava/script/tests/test_commands.py59
-rw-r--r--lava/script/tests/test_script.py80
-rw-r--r--lava/testdef/__init__.py82
-rw-r--r--lava/testdef/commands.py104
-rw-r--r--lava/testdef/templates.py52
-rw-r--r--lava/testdef/tests/__init__.py0
-rw-r--r--lava/testdef/tests/test_commands.py159
-rw-r--r--lava/tests/__init__.py0
-rw-r--r--lava/tests/test_commands.py128
-rw-r--r--lava/tests/test_config.py320
-rw-r--r--lava/tests/test_parameter.py206
-rw-r--r--lava/tool/__init__.py2
-rw-r--r--lava/tool/commands/__init__.py83
-rw-r--r--lava/tool/dispatcher.py12
-rw-r--r--lava/tool/errors.py2
-rw-r--r--lava/tool/main.py6
43 files changed, 4069 insertions, 89 deletions
diff --git a/lava/commands.py b/lava/commands.py
new file mode 100644
index 0000000..86f9afb
--- /dev/null
+++ b/lava/commands.py
@@ -0,0 +1,227 @@
+# Copyright (C) 2013 Linaro Limited
+#
+# Author: Milo Casagrande <milo.casagrande@linaro.org>
+#
+# This file is part of lava-tool.
+#
+# lava-tool is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License version 3
+# as published by the Free Software Foundation
+#
+# lava-tool is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with lava-tool. If not, see <http://www.gnu.org/licenses/>.
+
+"""
+Lava init commands.
+
+When invoking:
+
+ `lava init [DIR]`
+
+the command will create a default directory and files structure as follows:
+
+DIR/
+ |
+ +- JOB_FILE.json
+ +- tests/
+ |
+ + mytest.sh
+ + lavatest.yaml
+
+If DIR is not passed, it will use the current working directory.
+JOB_FILE is a file name that will be asked to the user, along with
+other necessary information to define the tests.
+
+If the user manually updates either the lavatest.yaml or mytest.sh file, it is
+necessary to run the following command in order to update the job definition:
+
+ `lava update [JOB|DIR]`
+"""
+
+import copy
+import json
+import os
+import sys
+
+from lava.helper.command import BaseCommand
+from lava.helper.template import (
+ expand_template,
+ set_value
+)
+from lava.job import (
+ JOB_FILE_EXTENSIONS,
+)
+from lava.job.templates import (
+ LAVA_TEST_SHELL_TAR_REPO_KEY,
+)
+from lava.parameter import (
+ Parameter,
+)
+from lava.testdef import (
+ DEFAULT_TESTDEF_FILENAME,
+)
+from lava.tool.errors import CommandError
+from lava_tool.utils import (
+ base64_encode,
+ create_dir,
+ create_tar,
+ edit_file,
+ retrieve_file,
+ write_file,
+)
+
+# Default directory structure name.
+TESTS_DIR = "tests"
+
+# Internal parameter ids.
+JOBFILE_ID = "jobfile"
+
+JOBFILE_PARAMETER = Parameter(JOBFILE_ID)
+JOBFILE_PARAMETER.store = False
+
+INIT_TEMPLATE = {
+ JOBFILE_ID: JOBFILE_PARAMETER,
+}
+
+
+class init(BaseCommand):
+ """Set-ups the base directory structure."""
+
+ @classmethod
+ def register_arguments(cls, parser):
+ super(init, cls).register_arguments(parser)
+ parser.add_argument("DIR",
+ help=("The name of the directory to initialize. "
+ "Defaults to current working directory."),
+ nargs="?",
+ default=os.getcwd())
+
+ def invoke(self):
+ full_path = os.path.abspath(self.args.DIR)
+
+ if os.path.isfile(full_path):
+ raise CommandError("'{0}' already exists, and is a "
+ "file.".format(self.args.DIR))
+
+ create_dir(full_path)
+ data = self._update_data()
+
+ # Create the directory that will contain the test definition and
+ # shell script.
+ test_path = create_dir(full_path, TESTS_DIR)
+ shell_script = self.create_shell_script(test_path)
+ # Let the user modify the file.
+ edit_file(shell_script)
+
+ testdef_file = self.create_test_definition(
+ os.path.join(test_path, DEFAULT_TESTDEF_FILENAME))
+
+ job = data[JOBFILE_ID]
+ self.create_tar_repo_job(
+ os.path.join(full_path, job), testdef_file, test_path)
+
+ def _update_data(self):
+ """Updates the template and ask values to the user.
+
+ The template in this case is a layout of the directory structure as it
+ would be written to disk.
+
+ :return A dictionary containing all the necessary file names to create.
+ """
+ data = copy.deepcopy(INIT_TEMPLATE)
+ expand_template(data, self.config)
+
+ return data
+
+
+class run(BaseCommand):
+ """Runs a job on the local dispatcher."""
+
+ @classmethod
+ def register_arguments(cls, parser):
+ super(run, cls).register_arguments(parser)
+ parser.add_argument("JOB",
+ help=("The job file to run, or a directory "
+ "containing a job file. If nothing is "
+ "passed, it uses the current working "
+ "directory."),
+ nargs="?",
+ default=os.getcwd())
+
+ def invoke(self):
+ full_path = os.path.abspath(self.args.JOB)
+ job_file = retrieve_file(full_path, JOB_FILE_EXTENSIONS)
+
+ super(run, self).run(job_file)
+
+
+class submit(BaseCommand):
+ """Submits a job to LAVA."""
+
+ @classmethod
+ def register_arguments(cls, parser):
+ super(submit, cls).register_arguments(parser)
+ parser.add_argument("JOB",
+ help=("The job file to send, or a directory "
+ "containing a job file. If nothing is "
+ "passed, it uses the current working "
+ "directory."),
+ nargs="?",
+ default=os.getcwd())
+
+ def invoke(self):
+ full_path = os.path.abspath(self.args.JOB)
+ job_file = retrieve_file(full_path, JOB_FILE_EXTENSIONS)
+
+ super(submit, self).submit(job_file)
+
+
+class update(BaseCommand):
+ """Updates a job file with the correct data."""
+
+ @classmethod
+ def register_arguments(cls, parser):
+ super(update, cls).register_arguments(parser)
+ parser.add_argument("JOB",
+ help=("Automatically updates a job file "
+ "definition. If nothing is passed, it uses"
+ "the current working directory."),
+ nargs="?",
+ default=os.getcwd())
+
+ def invoke(self):
+ full_path = os.path.abspath(self.args.JOB)
+ job_file = self.retrieve_file(full_path, JOB_FILE_EXTENSIONS)
+ job_dir = os.path.dirname(job_file)
+ tests_dir = os.path.join(job_dir, TESTS_DIR)
+
+ if os.path.isdir(tests_dir):
+ tar_repo = None
+ try:
+ tar_repo = create_tar(tests_dir)
+ encoded_tests = base64_encode(tar_repo)
+
+ json_data = None
+ with open(job_file, "r") as json_file:
+ try:
+ json_data = json.load(json_file)
+ set_value(json_data, LAVA_TEST_SHELL_TAR_REPO_KEY,
+ encoded_tests)
+ except Exception:
+ raise CommandError("Cannot read job file "
+ "'{0}'.".format(job_file))
+
+ content = json.dumps(json_data, indent=4)
+ write_file(job_file, content)
+
+ print >> sys.stdout, "Job definition updated."
+ finally:
+ if tar_repo and os.path.isfile(tar_repo):
+ os.unlink(tar_repo)
+ else:
+ raise CommandError("Cannot find tests directory.")
diff --git a/lava/config.py b/lava/config.py
new file mode 100644
index 0000000..1fb517f
--- /dev/null
+++ b/lava/config.py
@@ -0,0 +1,294 @@
+# Copyright (C) 2013 Linaro Limited
+#
+# Author: Antonio Terceiro <antonio.terceiro@linaro.org>
+#
+# This file is part of lava-tool.
+#
+# lava-tool is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License version 3
+# as published by the Free Software Foundation
+#
+# lava-tool is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with lava-tool. If not, see <http://www.gnu.org/licenses/>.
+
+"""
+Config class.
+"""
+
+import atexit
+import os
+import readline
+import xdg.BaseDirectory as xdgBaseDir
+
+from ConfigParser import (
+ ConfigParser,
+ NoOptionError,
+ NoSectionError,
+)
+
+from lava.parameter import Parameter
+from lava.tool.errors import CommandError
+
+__all__ = ['Config', 'InteractiveCache', 'InteractiveConfig']
+
+# Store for function calls to be made at exit time.
+AT_EXIT_CALLS = set()
+# Config default section.
+DEFAULT_SECTION = "DEFAULT"
+# This is the default base name used to create XDG resources.
+DEFAULT_XDG_RESOURCE = "linaro"
+# This is the default name for lava-tool resources.
+DEFAULT_LAVA_TOOL_RESOURCE = "lava-tool"
+
+HISTORY = os.path.join(os.path.expanduser("~"), ".lava_history")
+try:
+ readline.read_history_file(HISTORY)
+except IOError:
+ pass
+atexit.register(readline.write_history_file, HISTORY)
+
+
+def _run_at_exit():
+ """Runs all the function at exit."""
+ for call in list(AT_EXIT_CALLS):
+ call()
+atexit.register(_run_at_exit)
+
+
+class Config(object):
+ """A generic config object."""
+
+ def __init__(self):
+ # The cache where to store parameters.
+ self._cache = {}
+ self._config_file = None
+ self._config_backend = None
+ AT_EXIT_CALLS.add(self.save)
+
+ @property
+ def config_file(self):
+ if self._config_file is None:
+ self._config_file = (os.environ.get('LAVACONFIG') or
+ os.path.join(self._ensure_xdg_dirs(),
+ 'lava-tool.ini'))
+ return self._config_file
+
+ @config_file.setter
+ def config_file(self, value):
+ self._config_file = value
+
+ @property
+ def config_backend(self):
+ if self._config_backend is None:
+ self._config_backend = ConfigParser()
+ self._config_backend.read([self.config_file])
+ return self._config_backend
+
+ def _ensure_xdg_dirs(self):
+ """Make sure we have the default resource.
+
+ :return The path to the XDG resource.
+ """
+ return xdgBaseDir.save_config_path(DEFAULT_XDG_RESOURCE,
+ DEFAULT_LAVA_TOOL_RESOURCE)
+
+ def _calculate_config_section(self, parameter):
+ """Calculates the config section of the specified parameter.
+
+ :param parameter: The parameter to calculate the section of.
+ :type Parameter
+ :return The config section.
+ """
+ section = DEFAULT_SECTION
+ if parameter.depends:
+ section = "{0}={1}".format(parameter.depends.id,
+ self.get(parameter.depends))
+ return section
+
+ def get(self, parameter, section=None):
+ """Retrieves a Parameter value.
+
+ The value is taken either from the Parameter itself, or from the cache,
+ or from the config file.
+
+ :param parameter: The parameter to search.
+ :type Parameter
+ :return The parameter value, or None if it is not found.
+ """
+ if not section:
+ section = self._calculate_config_section(parameter)
+ # Try to get the parameter value first if it has one.
+ if parameter.value is not None:
+ value = parameter.value
+ else:
+ value = self._get_from_cache(parameter, section)
+
+ if value is None:
+ value = self._get_from_backend(parameter, section)
+ return value
+
+ def get_from_backend(self, parameter, section=None):
+ """Gets a configuration parameter directly from the config file."""
+ if not section:
+ section = self._calculate_config_section(parameter)
+ return self._get_from_backend(parameter, section)
+
+ def _get_from_backend(self, parameter, section):
+ """Gets the parameter value from the config backend.
+
+ :param parameter: The Parameter to look up.
+ :param section: The section in the Config.
+ """
+ value = None
+ try:
+ value = self.config_backend.get(section, parameter.id)
+ except (NoOptionError, NoSectionError):
+ # Ignore, we return None.
+ pass
+ return value
+
+ def _get_from_cache(self, parameter, section):
+ """Looks for the specified parameter in the internal cache.
+
+ :param parameter: The parameter to search.
+ :type Parameter
+ :return The parameter value, of None if it is not found.
+ """
+ value = None
+ if section in self._cache.keys():
+ if parameter.id in self._cache[section].keys():
+ value = self._cache[section][parameter.id]
+ return value
+
+ def _put_in_cache(self, key, value, section=DEFAULT_SECTION):
+ """Insert the passed parameter in the internal cache.
+
+ :param parameter: The parameter to insert.
+ :type Parameter
+ :param section: The name of the section in the config file.
+ :type str
+ """
+ if section not in self._cache.keys():
+ self._cache[section] = {}
+ self._cache[section][key] = value
+
+ def put(self, key, value, section=DEFAULT_SECTION):
+ """Adds a parameter to the config file.
+
+ :param key: The key to add.
+ :param value: The value to add.
+ :param section: The name of the section as in the config file.
+ """
+ if (not self.config_backend.has_section(section) and
+ section != DEFAULT_SECTION):
+ self.config_backend.add_section(section)
+
+ # This is done to serialize a list when ConfigParser is written to
+ # file. Since there is no real support for list in ConfigParser, we
+ # serialized it in a common way that can get easily deserialized.
+ if isinstance(value, list):
+ value = Parameter.serialize(value)
+
+ self.config_backend.set(section, key, value)
+ # Store in the cache too.
+ self._put_in_cache(key, value, section)
+
+ def put_parameter(self, parameter, value=None, section=None):
+ """Adds a Parameter to the config file and cache.
+
+ :param Parameter: The parameter to add.
+ :type Parameter
+ :param value: The value of the parameter. Defaults to None.
+ :param section: The section where this parameter should be stored.
+ Defaults to None.
+ """
+ if not section:
+ section = self._calculate_config_section(parameter)
+
+ if value is None and parameter.value is not None:
+ value = parameter.value
+ elif value is None:
+ raise CommandError("No value assigned to '{0}'.".format(
+ parameter.id))
+ self.put(parameter.id, value, section)
+
+ def save(self):
+ """Saves the config to file."""
+ # Since we lazy load the config_backend property, this check is needed
+ # when a user enters a wrong command or it will overwrite the 'config'
+ # file with empty contents.
+ if self._config_backend:
+ with open(self.config_file, "w") as write_file:
+ self.config_backend.write(write_file)
+
+
+class InteractiveConfig(Config):
+ """An interactive config.
+
+ If a value is not found in the config file, it will ask it and then stores
+ it.
+ """
+ def __init__(self, force_interactive=True):
+ super(InteractiveConfig, self).__init__()
+ self._force_interactive = force_interactive
+
+ @property
+ def force_interactive(self):
+ return self._force_interactive
+
+ @force_interactive.setter
+ def force_interactive(self, value):
+ self._force_interactive = value
+
+ def get(self, parameter, section=None):
+ """Overrides the parent one.
+
+ The only difference with the parent one, is that it will ask to type
+ a parameter value in case it is not found.
+ """
+ if not section:
+ section = self._calculate_config_section(parameter)
+ value = super(InteractiveConfig, self).get(parameter, section)
+
+ if value is None or self.force_interactive:
+ value = parameter.prompt(old_value=value)
+
+ if value is not None and parameter.store:
+ self.put(parameter.id, value, section)
+ return value
+
+
+class InteractiveCache(InteractiveConfig):
+
+ """An interactive cache where parameters that can change are stored.
+
+ This class is basically the same as the Confing and InteractiveConfig ones,
+ only the base directory where the cache file is stored is different.
+
+ In this case it will use the $XDG_CACHE_HOME value as defined in XDG.
+ """
+
+ @property
+ def config_file(self):
+ if self._config_file is None:
+ self._config_file = (os.environ.get('LAVACACHE') or
+ os.path.join(self._ensure_xdg_dirs(),
+ 'parameters.ini'))
+ return self._config_file
+
+ @config_file.setter
+ def config_file(self, value):
+ self._config_file = value
+
+ def _ensure_xdg_dirs(self):
+ """Make sure we have the default resource.
+
+ :return The path to the XDG resource.
+ """
+ return xdgBaseDir.save_cache_path(DEFAULT_XDG_RESOURCE,
+ DEFAULT_LAVA_TOOL_RESOURCE)
diff --git a/lava/device/__init__.py b/lava/device/__init__.py
new file mode 100644
index 0000000..35fe181
--- /dev/null
+++ b/lava/device/__init__.py
@@ -0,0 +1,97 @@
+# Copyright (C) 2013 Linaro Limited
+#
+# Author: Milo Casagrande <milo.casagrande@linaro.org>
+#
+# This file is part of lava-tool.
+#
+# lava-tool is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License version 3
+# as published by the Free Software Foundation
+#
+# lava-tool is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with lava-tool. If not, see <http://www.gnu.org/licenses/>.
+
+"""Device class."""
+
+import re
+
+from copy import deepcopy
+
+from lava.device.templates import (
+ DEFAULT_TEMPLATE,
+ HOSTNAME_PARAMETER,
+ KNOWN_TEMPLATES,
+)
+from lava.helper.template import expand_template
+
+
+def __re_compile(name):
+ """Creates a generic regex for the specified device name.
+
+ :param name: The name of the device.
+ :return A Pattern object.
+ """
+ return re.compile('^.*{0}.*'.format(name), re.I)
+
+
+# Dictionary of know devices.
+# Keys are the general device name taken from lava.device.templates, values
+# are tuples of: a regex matcher to match the device, and the device associated
+# template.
+KNOWN_DEVICES = dict([(device, (__re_compile(device), template))
+ for device, template in KNOWN_TEMPLATES.iteritems()])
+
+
+class Device(object):
+
+ """A generic device."""
+
+ def __init__(self, data, hostname=None):
+ self.data = deepcopy(data)
+ self.hostname = hostname
+
+ def write(self, conf_file):
+ """Writes the object to file.
+
+ :param conf_file: The full path of the file where to write."""
+ with open(conf_file, 'w') as write_file:
+ write_file.write(str(self))
+
+ def update(self, config):
+ """Updates the Device object values based on the provided config.
+
+ :param config: A Config instance.
+ """
+ # We should always have a hostname, since it defaults to the name
+ # given on the command line for the config file.
+ if self.hostname is not None:
+ # We do not ask the user again this parameter.
+ self.data[HOSTNAME_PARAMETER.id].value = self.hostname
+ self.data[HOSTNAME_PARAMETER.id].asked = True
+
+ expand_template(self.data, config)
+
+ def __str__(self):
+ string_list = []
+ for key, value in self.data.iteritems():
+ string_list.append("{0} = {1}\n".format(str(key), str(value)))
+ return "".join(string_list)
+
+
+def get_known_device(name):
+ """Tries to match a device name with a known device type.
+
+ :param name: The name of the device we want matched to a real device.
+ :return A Device instance.
+ """
+ instance = Device(DEFAULT_TEMPLATE, hostname=name)
+ for _, (matcher, dev_template) in KNOWN_DEVICES.iteritems():
+ if matcher.match(name):
+ instance = Device(dev_template, hostname=name)
+ break
+ return instance
diff --git a/lava/device/commands.py b/lava/device/commands.py
new file mode 100644
index 0000000..a8ce66d
--- /dev/null
+++ b/lava/device/commands.py
@@ -0,0 +1,122 @@
+# Copyright (C) 2013 Linaro Limited
+#
+# Author: Milo Casagrande <milo.casagrande@linaro.org>
+#
+# This file is part of lava-tool.
+#
+# lava-tool is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License version 3
+# as published by the Free Software Foundation
+#
+# lava-tool is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with lava-tool. If not, see <http://www.gnu.org/licenses/>.
+
+"""
+Device specific commands class.
+"""
+
+import os
+import sys
+
+from lava.device import get_known_device
+from lava.helper.command import (
+ BaseCommand,
+)
+from lava.helper.dispatcher import (
+ get_device_file,
+ get_devices_path,
+)
+from lava.tool.command import CommandGroup
+from lava.tool.errors import CommandError
+from lava_tool.utils import (
+ can_edit_file,
+ edit_file,
+)
+
+DEVICE_FILE_SUFFIX = "conf"
+
+
+class device(CommandGroup):
+ """LAVA devices handling."""
+
+ namespace = "lava.device.commands"
+
+
+class add(BaseCommand):
+ """Adds a new device."""
+
+ @classmethod
+ def register_arguments(cls, parser):
+ super(add, cls).register_arguments(parser)
+ parser.add_argument("DEVICE", help="The name of the device to add.")
+
+ def invoke(self):
+ real_file_name = ".".join([self.args.DEVICE, DEVICE_FILE_SUFFIX])
+
+ if get_device_file(real_file_name) is not None:
+ print >> sys.stdout, ("A device configuration file named '{0}' "
+ "already exists.".format(real_file_name))
+ print >> sys.stdout, ("Use 'lava device config {0}' to edit "
+ "it.".format(self.args.DEVICE))
+ sys.exit(-1)
+
+ devices_path = get_devices_path()
+ device_conf_file = os.path.abspath(os.path.join(devices_path,
+ real_file_name))
+
+ device = get_known_device(self.args.DEVICE)
+ device.update(self.config)
+ device.write(device_conf_file)
+
+ print >> sys.stdout, ("Created device file '{0}' in: {1}".format(
+ real_file_name, devices_path))
+ edit_file(device_conf_file)
+
+
+class remove(BaseCommand):
+ """Removes the specified device."""
+
+ @classmethod
+ def register_arguments(cls, parser):
+ super(remove, cls).register_arguments(parser)
+ parser.add_argument("DEVICE",
+ help="The name of the device to remove.")
+
+ def invoke(self):
+ real_file_name = ".".join([self.args.DEVICE, DEVICE_FILE_SUFFIX])
+ device_conf = get_device_file(real_file_name)
+
+ if device_conf:
+ try:
+ os.remove(device_conf)
+ print >> sys.stdout, ("Device configuration file '{0}' "
+ "removed.".format(real_file_name))
+ except OSError:
+ raise CommandError("Cannot remove file '{0}' at: {1}.".format(
+ real_file_name, os.path.dirname(device_conf)))
+ else:
+ print >> sys.stdout, ("No device configuration file '{0}' "
+ "found.".format(real_file_name))
+
+
+class config(BaseCommand):
+ """Opens the specified device config file."""
+ @classmethod
+ def register_arguments(cls, parser):
+ super(config, cls).register_arguments(parser)
+ parser.add_argument("DEVICE",
+ help="The name of the device to edit.")
+
+ def invoke(self):
+ real_file_name = ".".join([self.args.DEVICE, DEVICE_FILE_SUFFIX])
+ device_conf = get_device_file(real_file_name)
+
+ if device_conf and can_edit_file(device_conf):
+ edit_file(device_conf)
+ else:
+ raise CommandError("Cannot edit file '{0}'".format(real_file_name))
diff --git a/lava/device/templates.py b/lava/device/templates.py
new file mode 100644
index 0000000..e260117
--- /dev/null
+++ b/lava/device/templates.py
@@ -0,0 +1,82 @@
+# Copyright (C) 2013 Linaro Limited
+#
+# Author: Milo Casagrande <milo.casagrande@linaro.org>
+#
+# This file is part of lava-tool.
+#
+# lava-tool is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License version 3
+# as published by the Free Software Foundation
+#
+# lava-tool is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with lava-tool. If not, see <http://www.gnu.org/licenses/>.
+
+"""
+This is just a place where to store a template like dictionary that
+will be used to serialize a Device object.
+"""
+
+from copy import copy
+
+from lava.parameter import Parameter
+
+# The hostname parameter is always in the DEFAULT config section.
+HOSTNAME_PARAMETER = Parameter("hostname")
+DEVICE_TYPE_PARAMETER = Parameter("device_type", depends=HOSTNAME_PARAMETER)
+CONNECTION_COMMAND_PARMAETER = Parameter("connection_command",
+ depends=DEVICE_TYPE_PARAMETER)
+
+DEFAULT_TEMPLATE = {
+ 'hostname': HOSTNAME_PARAMETER,
+ 'device_type': DEVICE_TYPE_PARAMETER,
+ 'connection_command': CONNECTION_COMMAND_PARMAETER,
+}
+
+# Specialized copies of the parameters.
+# We need this or we might end up asking the user twice the same parameter due
+# to different object references when one Parameter depends on a "specialized"
+# one, different from the defaults.
+PANDA_DEVICE_TYPE = copy(DEVICE_TYPE_PARAMETER)
+PANDA_DEVICE_TYPE.value = "panda"
+PANDA_DEVICE_TYPE.asked = True
+
+PANDA_CONNECTION_COMMAND = copy(CONNECTION_COMMAND_PARMAETER)
+PANDA_CONNECTION_COMMAND.depends = PANDA_DEVICE_TYPE
+
+VEXPRESS_DEVICE_TYPE = copy(DEVICE_TYPE_PARAMETER)
+VEXPRESS_DEVICE_TYPE.value = "vexpress"
+VEXPRESS_DEVICE_TYPE.asked = True
+
+VEXPRESS_CONNECTION_COMMAND = copy(CONNECTION_COMMAND_PARMAETER)
+VEXPRESS_CONNECTION_COMMAND.depends = VEXPRESS_DEVICE_TYPE
+
+QEMU_DEVICE_TYPE = copy(DEVICE_TYPE_PARAMETER)
+QEMU_DEVICE_TYPE.value = "qemu"
+QEMU_DEVICE_TYPE.asked = True
+
+QEMU_CONNECTION_COMMAND = copy(CONNECTION_COMMAND_PARMAETER)
+QEMU_CONNECTION_COMMAND.depends = QEMU_DEVICE_TYPE
+
+# Dictionary with templates of known devices.
+KNOWN_TEMPLATES = {
+ 'panda': {
+ 'hostname': HOSTNAME_PARAMETER,
+ 'device_type': PANDA_DEVICE_TYPE,
+ 'connection_command': PANDA_CONNECTION_COMMAND,
+ },
+ 'vexpress': {
+ 'hostname': HOSTNAME_PARAMETER,
+ 'device_type': VEXPRESS_DEVICE_TYPE,
+ 'connection_command': VEXPRESS_CONNECTION_COMMAND,
+ },
+ 'qemu': {
+ 'hostname': HOSTNAME_PARAMETER,
+ 'device_type': QEMU_DEVICE_TYPE,
+ 'connection_command': QEMU_CONNECTION_COMMAND,
+ }
+}
diff --git a/lava/device/tests/__init__.py b/lava/device/tests/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/lava/device/tests/__init__.py
diff --git a/lava/device/tests/test_commands.py b/lava/device/tests/test_commands.py
new file mode 100644
index 0000000..91b204f
--- /dev/null
+++ b/lava/device/tests/test_commands.py
@@ -0,0 +1,182 @@
+# Copyright (C) 2013 Linaro Limited
+#
+# Author: Milo Casagrande <milo.casagrande@linaro.org>
+#
+# This file is part of lava-tool.
+#
+# lava-tool is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License version 3
+# as published by the Free Software Foundation
+#
+# lava-tool is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with lava-tool. If not, see <http://www.gnu.org/licenses/>.
+
+"""
+lava.device.commands unit tests.
+"""
+
+import os
+
+from mock import (
+ MagicMock,
+ call,
+ patch,
+)
+
+from lava.device.commands import (
+ add,
+ config,
+ remove,
+)
+from lava.helper.tests.helper_test import HelperTest
+from lava.tool.errors import CommandError
+
+
+class AddCommandTest(HelperTest):
+
+ def test_register_argument(self):
+ # Make sure that the parser add_argument is called and we have the
+ # correct argument.
+ add_command = add(self.parser, self.args)
+ add_command.register_arguments(self.parser)
+ name, args, kwargs = self.parser.method_calls[0]
+ self.assertIn("--non-interactive", args)
+
+ name, args, kwargs = self.parser.method_calls[1]
+ self.assertIn("DEVICE", args)
+
+ @patch("lava.device.commands.edit_file", create=True)
+ @patch("lava.device.Device.__str__")
+ @patch("lava.device.Device.update")
+ @patch("lava.device.commands.get_device_file")
+ @patch("lava.device.commands.get_devices_path")
+ def test_add_invoke_0(self, mocked_get_devices_path,
+ mocked_get_device_file, mocked_update, mocked_str,
+ mocked_edit_file):
+ # Tests invocation of the add command. Verifies that the conf file is
+ # written to disk.
+ mocked_get_devices_path.return_value = self.temp_dir
+ mocked_get_device_file.return_value = None
+ mocked_str.return_value = ""
+
+ add_command = add(self.parser, self.args)
+ add_command.invoke()
+
+ expected_path = os.path.join(self.temp_dir,
+ ".".join([self.device, "conf"]))
+ self.assertTrue(os.path.isfile(expected_path))
+
+ @patch("lava.device.commands.edit_file", create=True)
+ @patch("lava.device.commands.get_known_device")
+ @patch("lava.device.commands.get_devices_path")
+ @patch("lava.device.commands.sys.exit")
+ @patch("lava.device.commands.get_device_file")
+ def test_add_invoke_1(self, mocked_get_device_file, mocked_sys_exit,
+ mocked_get_devices_path, mocked_get_known_device,
+ mocked_edit_file):
+ mocked_get_devices_path.return_value = self.temp_dir
+ mocked_get_device_file.return_value = self.temp_file.name
+
+ add_command = add(self.parser, self.args)
+ add_command.invoke()
+
+ self.assertTrue(mocked_sys_exit.called)
+
+
+class RemoveCommandTests(HelperTest):
+
+ def test_register_argument(self):
+ # Make sure that the parser add_argument is called and we have the
+ # correct argument.
+ command = remove(self.parser, self.args)
+ command.register_arguments(self.parser)
+ name, args, kwargs = self.parser.method_calls[0]
+ self.assertIn("--non-interactive", args)
+
+ name, args, kwargs = self.parser.method_calls[1]
+ self.assertIn("DEVICE", args)
+
+ @patch("lava.device.commands.edit_file", create=True)
+ @patch("lava.device.Device.__str__", return_value="")
+ @patch("lava.device.Device.update")
+ @patch("lava.device.commands.get_device_file")
+ @patch("lava.device.commands.get_devices_path")
+ def test_remove_invoke(self, get_devices_path_mock, get_device_file_mock,
+ mocked_update, mocked_str, mocked_edit_file):
+ # Tests invocation of the remove command. Verifies that the conf file
+ # has been correctly removed.
+ # First we add a new conf file, then we remove it.
+ get_device_file_mock.return_value = None
+ get_devices_path_mock.return_value = self.temp_dir
+
+ add_command = add(self.parser, self.args)
+ add_command.invoke()
+
+ expected_path = os.path.join(self.temp_dir,
+ ".".join([self.device, "conf"]))
+
+ # Set new values for the mocked function.
+ get_device_file_mock.return_value = expected_path
+
+ remove_command = remove(self.parser, self.args)
+ remove_command.invoke()
+
+ self.assertFalse(os.path.isfile(expected_path))
+
+ @patch("lava.device.commands.get_device_file",
+ new=MagicMock(return_value="/root"))
+ def test_remove_invoke_raises(self):
+ # Tests invocation of the remove command, with a non existent device
+ # configuration file.
+ remove_command = remove(self.parser, self.args)
+ self.assertRaises(CommandError, remove_command.invoke)
+
+
+class ConfigCommanTests(HelperTest):
+
+ def test_register_argument(self):
+ # Make sure that the parser add_argument is called and we have the
+ # correct argument.
+ command = config(self.parser, self.args)
+ command.register_arguments(self.parser)
+ name, args, kwargs = self.parser.method_calls[0]
+ self.assertIn("--non-interactive", args)
+
+ name, args, kwargs = self.parser.method_calls[1]
+ self.assertIn("DEVICE", args)
+
+ @patch("lava.device.commands.can_edit_file", create=True)
+ @patch("lava.device.commands.edit_file", create=True)
+ @patch("lava.device.commands.get_device_file")
+ def test_config_invoke_0(self, mocked_get_device_file, mocked_edit_file,
+ mocked_can_edit_file):
+ command = config(self.parser, self.args)
+
+ mocked_can_edit_file.return_value = True
+ mocked_get_device_file.return_value = self.temp_file.name
+ command.invoke()
+
+ self.assertTrue(mocked_edit_file.called)
+ self.assertEqual([call(self.temp_file.name)],
+ mocked_edit_file.call_args_list)
+
+ @patch("lava.device.commands.get_device_file",
+ new=MagicMock(return_value=None))
+ def test_config_invoke_raises_0(self):
+ # Tests invocation of the config command, with a non existent device
+ # configuration file.
+ config_command = config(self.parser, self.args)
+ self.assertRaises(CommandError, config_command.invoke)
+
+ @patch("lava.device.commands.get_device_file",
+ new=MagicMock(return_value="/etc/password"))
+ def test_config_invoke_raises_1(self):
+ # Tests invocation of the config command, with a non writable file.
+ # Hopefully tests are not run as root.
+ config_command = config(self.parser, self.args)
+ self.assertRaises(CommandError, config_command.invoke)
diff --git a/lava/device/tests/test_device.py b/lava/device/tests/test_device.py
new file mode 100644
index 0000000..c8185f4
--- /dev/null
+++ b/lava/device/tests/test_device.py
@@ -0,0 +1,119 @@
+# Copyright (C) 2013 Linaro Limited
+#
+# Author: Milo Casagrande <milo.casagrande@linaro.org>
+#
+# This file is part of lava-tool.
+#
+# lava-tool is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License version 3
+# as published by the Free Software Foundation
+#
+# lava-tool is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with lava-tool. If not, see <http://www.gnu.org/licenses/>.
+
+"""
+Device class unit tests.
+"""
+
+from mock import patch
+
+from lava.config import Config
+from lava.device import (
+ Device,
+ get_known_device,
+)
+from lava.device.templates import (
+ HOSTNAME_PARAMETER,
+ PANDA_DEVICE_TYPE,
+ PANDA_CONNECTION_COMMAND,
+)
+from lava.helper.tests.helper_test import HelperTest
+from lava.parameter import Parameter
+
+
+class DeviceTest(HelperTest):
+
+ def test_get_known_device_panda_0(self):
+ # User creates a new device with a guessable name for a device.
+ instance = get_known_device('panda_new_01')
+ self.assertIsInstance(instance, Device)
+ self.assertEqual(instance.data['device_type'].value, 'panda')
+
+ def test_get_known_device_panda_1(self):
+ # User creates a new device with a guessable name for a device.
+ # Name passed has capital letters.
+ instance = get_known_device('new_PanDa_02')
+ self.assertIsInstance(instance, Device)
+ self.assertEqual(instance.data['device_type'].value, 'panda')
+
+ def test_get_known_device_vexpress_0(self):
+ # User creates a new device with a guessable name for a device.
+ # Name passed has capital letters.
+ instance = get_known_device('a_VexPress_Device')
+ self.assertIsInstance(instance, Device)
+ self.assertEqual(instance.data['device_type'].value, 'vexpress')
+
+ def test_get_known_device_vexpress_1(self):
+ # User creates a new device with a guessable name for a device.
+ instance = get_known_device('another-vexpress')
+ self.assertIsInstance(instance, Device)
+ self.assertIsInstance(instance.data['device_type'], Parameter)
+ self.assertEqual(instance.data['device_type'].value, 'vexpress')
+
+ @patch("lava.config.Config.save")
+ def test_device_update_1(self, patched_save):
+ # Tests that when calling update() on a Device, the template gets
+ # updated with the correct values from a Config instance.
+ hostname = "panda_device"
+
+ config = Config()
+ config._config_file = self.temp_file.name
+ config.put_parameter(HOSTNAME_PARAMETER, hostname)
+ config.put_parameter(PANDA_DEVICE_TYPE, "panda")
+ config.put_parameter(PANDA_CONNECTION_COMMAND, "test")
+
+ expected = {
+ "hostname": hostname,
+ "device_type": "panda",
+ "connection_command": "test"
+ }
+
+ instance = get_known_device(hostname)
+ instance.update(config)
+
+ self.assertEqual(expected, instance.data)
+
+ @patch("lava.config.Config.save")
+ def test_device_write(self, mocked_save):
+ # User tries to create a new panda device. The conf file is written
+ # and contains the expected results.
+ hostname = "panda_device"
+
+ config = Config()
+ config._config_file = self.temp_file.name
+ config.put_parameter(HOSTNAME_PARAMETER, hostname)
+ config.put_parameter(PANDA_DEVICE_TYPE, "panda")
+ config.put_parameter(PANDA_CONNECTION_COMMAND, "test")
+
+ expected = {
+ "hostname": hostname,
+ "device_type": "panda",
+ "connection_command": "test"
+ }
+
+ instance = get_known_device(hostname)
+ instance.update(config)
+ instance.write(self.temp_file.name)
+
+ expected = ("hostname = panda_device\nconnection_command = test\n"
+ "device_type = panda\n")
+
+ obtained = ""
+ with open(self.temp_file.name) as f:
+ obtained = f.read()
+ self.assertEqual(expected, obtained)
diff --git a/lava/helper/__init__.py b/lava/helper/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/lava/helper/__init__.py
diff --git a/lava/helper/command.py b/lava/helper/command.py
new file mode 100644
index 0000000..a990f29
--- /dev/null
+++ b/lava/helper/command.py
@@ -0,0 +1,244 @@
+# Copyright (C) 2013 Linaro Limited
+#
+# Author: Milo Casagrande <milo.casagrande@linaro.org>
+#
+# This file is part of lava-tool.
+#
+# lava-tool is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License version 3
+# as published by the Free Software Foundation
+#
+# lava-tool is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with lava-tool. If not, see <http://www.gnu.org/licenses/>.
+
+"""Base command class common to lava commands series."""
+
+import os
+import sys
+import xmlrpclib
+
+from lava.config import (
+ InteractiveCache,
+)
+from lava.helper.dispatcher import get_devices
+from lava.job import Job
+from lava.job.templates import (
+ LAVA_TEST_SHELL_TAR_REPO,
+ LAVA_TEST_SHELL_TAR_REPO_KEY,
+ LAVA_TEST_SHELL_TESDEF_KEY,
+)
+from lava.parameter import (
+ Parameter,
+ SingleChoiceParameter,
+)
+from lava.script import (
+ ShellScript,
+ DEFAULT_TESTDEF_SCRIPT,
+)
+from lava.testdef import TestDefinition
+from lava.testdef.templates import (
+ TESTDEF_STEPS_KEY,
+ TESTDEF_TEMPLATE,
+)
+from lava.tool.command import Command
+from lava.tool.errors import CommandError
+from lava_tool.authtoken import (
+ AuthenticatingServerProxy,
+ KeyringAuthBackend
+)
+from lava_tool.utils import (
+ base64_encode,
+ create_tar,
+ execute,
+ has_command,
+ to_list,
+ verify_and_create_url,
+)
+
+CONFIG = InteractiveCache()
+
+
+class BaseCommand(Command):
+
+ """Base command class for all lava commands."""
+
+ def __init__(self, parser, args):
+ super(BaseCommand, self).__init__(parser, args)
+ self.config = CONFIG
+ self.config.force_interactive = self.args.non_interactive
+
+ @classmethod
+ def register_arguments(cls, parser):
+ super(BaseCommand, cls).register_arguments(parser)
+ parser.add_argument("--non-interactive", "-n",
+ action='store_false',
+ help=("Do not ask for input parameters."))
+
+ def authenticated_server(self):
+ """Returns a connection to a LAVA server.
+
+ It will ask the user the necessary parameters to establish the
+ connection.
+ """
+ print >> sys.stdout, "\nServer connection parameters:"
+
+ server_name_parameter = Parameter("server")
+ rpc_endpoint_parameter = Parameter("rpc_endpoint",
+ depends=server_name_parameter)
+
+ self.config.get(server_name_parameter)
+ endpoint = self.config.get(rpc_endpoint_parameter)
+
+ rpc_url = verify_and_create_url(endpoint)
+ server = AuthenticatingServerProxy(rpc_url,
+ auth_backend=KeyringAuthBackend())
+ return server
+
+ def submit(self, job_file):
+ """Submits a job file to a LAVA server.
+
+ :param job_file: The job file to submit.
+ :return The job ID on success.
+ """
+ if os.path.isfile(job_file):
+ try:
+ jobdata = open(job_file, 'rb').read()
+ server = self.authenticated_server()
+
+ job_id = server.scheduler.submit_job(jobdata)
+ print >> sys.stdout, ("Job submitted with job "
+ "ID {0}.".format(job_id))
+
+ return job_id
+ except xmlrpclib.Fault, exc:
+ raise CommandError(str(exc))
+ else:
+ raise CommandError("Job file '{0}' does not exists, or is not "
+ "a file.".format(job_file))
+
+ def run(self, job_file):
+ """Runs a job file on the local LAVA dispatcher.
+
+ :param job_file: The job file to run.
+ """
+ if os.path.isfile(job_file):
+ if has_command("lava-dispatch"):
+ devices = get_devices()
+ if devices:
+ if len(devices) > 1:
+ device_names = [device.hostname for device in devices]
+ device_param = SingleChoiceParameter("device",
+ device_names)
+ device = device_param.prompt("Device to use: ")
+ else:
+ device = devices[0].hostname
+ execute(["lava-dispatch", "--target", device, job_file])
+ else:
+ raise CommandError("Cannot find lava-dispatcher installation.")
+ else:
+ raise CommandError("Job file '{0}' does not exists, or it is not "
+ "a file.".format(job_file))
+
+ def status(self, job_id):
+ """Retrieves the status of a LAVA job.
+
+ :param job_id: The ID of the job to look up.
+ """
+ job_id = str(job_id)
+
+ try:
+ server = self.authenticated_server()
+ job_status = server.scheduler.job_status(job_id)
+
+ status = job_status["job_status"].lower()
+ bundle = job_status["bundle_sha1"]
+
+ print >> sys.stdout, "\nJob id: {0}".format(job_id)
+ print >> sys.stdout, "Status: {0}".format(status)
+ print >> sys.stdout, "Bundle: {0}".format(bundle)
+ except xmlrpclib.Fault, exc:
+ raise CommandError(str(exc))
+
+ def create_tar_repo_job(self, job_file, testdef_file, tar_content):
+ """Creates a job file based on the tar-repo template.
+
+ The tar repo is not kept on the file system.
+
+ :param job_file: The path of the job file to create.
+ :param testdef_file: The path of the test definition file.
+ :param tar_content: What should go into the tarball repository.
+ :return The path of the job file created.
+ """
+
+ print >> sys.stdout, "\nCreating job file..."
+
+ try:
+ tar_repo = create_tar(tar_content)
+
+ job_instance = Job(LAVA_TEST_SHELL_TAR_REPO, job_file)
+ job_instance.update(self.config)
+
+ job_instance.set(LAVA_TEST_SHELL_TAR_REPO_KEY,
+ base64_encode(tar_repo))
+ job_instance.set(LAVA_TEST_SHELL_TESDEF_KEY,
+ os.path.basename(testdef_file))
+
+ job_instance.write()
+
+ basename = os.path.basename(job_instance.file_name)
+ print >> sys.stdout, ("\nCreated job file "
+ "'{0}'.".format(basename))
+
+ return job_instance.file_name
+ finally:
+ if os.path.isfile(tar_repo):
+ os.unlink(tar_repo)
+
+ def create_test_definition(self, testdef_file, template=TESTDEF_TEMPLATE,
+ steps=None):
+ """Creates a test definition YAML file.
+
+ :param testdef_file: The file to create.
+ :return The path of the file created.
+ """
+
+ print >> sys.stdout, "\nCreating test definition file..."
+
+ testdef = TestDefinition(template, testdef_file)
+ if steps:
+ steps = to_list(steps)
+ testdef.set(TESTDEF_STEPS_KEY, steps)
+ testdef.update(self.config)
+ testdef.write()
+
+ basename = os.path.basename(testdef.file_name)
+ print >> sys.stdout, ("\nCreated test definition "
+ "'{0}'.".format(basename))
+
+ return testdef.file_name
+
+ def create_shell_script(self, test_path,
+ script_name=DEFAULT_TESTDEF_SCRIPT):
+ """Creates a shell script with some default content.
+
+ :param test_path: The directory where to create the script.
+ :param script_name: The name of the script.
+ :return The full path to the script file.
+ """
+ default_script = os.path.join(test_path, script_name)
+
+ if not os.path.isfile(default_script):
+ print >> sys.stdout, "Creating shell script..."
+
+ shell_script = ShellScript(default_script)
+ shell_script.write()
+
+ print >> sys.stdout, ("\nCreated shell script "
+ "'{0}'.".format(script_name))
+
+ return default_script
diff --git a/lava/helper/dispatcher.py b/lava/helper/dispatcher.py
new file mode 100644
index 0000000..5da01a9
--- /dev/null
+++ b/lava/helper/dispatcher.py
@@ -0,0 +1,110 @@
+# Copyright (C) 2013 Linaro Limited
+#
+# Author: Milo Casagrande <milo.casagrande@linaro.org>
+#
+# This file is part of lava-tool.
+#
+# lava-tool is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License version 3
+# as published by the Free Software Foundation
+#
+# lava-tool is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with lava-tool. If not, see <http://www.gnu.org/licenses/>.
+
+"""Classes and functions to interact with the lava-dispatcher."""
+
+import random
+import string
+import os
+
+from lava.tool.errors import CommandError
+
+# Default devices path, has to be joined with the dispatcher path.
+DEFAULT_DEVICES_PATH = "devices"
+
+
+def get_dispatcher_paths():
+ """Tries to get the dispatcher paths from lava-dispatcher.
+
+ :return A list of paths.
+ """
+ try:
+ from lava_dispatcher.config import write_path
+ return write_path()
+ except ImportError:
+ raise CommandError("Cannot find lava-dispatcher installation.")
+
+
+def get_devices():
+ """Gets the devices list from the dispatcher.
+
+ :return A list of DeviceConfig.
+ """
+ try:
+ from lava_dispatcher.config import get_devices
+ return get_devices()
+ except ImportError:
+ raise CommandError("Cannot find lava-dispatcher installation.")
+
+
+def get_device_file(file_name):
+ """Retrieves the config file name specified, if it exists.
+
+ :param file_name: The config file name to search.
+ :return The path to the file, or None if it does not exist.
+ """
+ try:
+ from lava_dispatcher.config import get_config_file
+ return get_config_file(os.path.join(DEFAULT_DEVICES_PATH,
+ file_name))
+ except ImportError:
+ raise CommandError("Cannot find lava-dispatcher installation.")
+
+
+def choose_devices_path(paths):
+ """Picks the first path that is writable by the user.
+
+ :param paths: A list of paths.
+ :return The first path where it is possible to write.
+ """
+ valid_path = None
+ for path in paths:
+ path = os.path.join(path, DEFAULT_DEVICES_PATH)
+ if os.path.exists(path):
+ name = "".join(random.choice(string.ascii_letters)
+ for x in range(6))
+ test_file = os.path.join(path, name)
+ try:
+ fp = open(test_file, 'a')
+ fp.close()
+ except IOError:
+ # Cannot write here.
+ continue
+ else:
+ valid_path = path
+ if os.path.isfile(test_file):
+ os.unlink(test_file)
+ break
+ else:
+ try:
+ os.makedirs(path)
+ except OSError:
+ # Cannot write here either.
+ continue
+ else:
+ valid_path = path
+ break
+ else:
+ raise CommandError("Insufficient permissions to create new "
+ "devices.")
+ return valid_path
+
+
+def get_devices_path():
+ """Gets the path to the devices in the LAVA dispatcher."""
+ return choose_devices_path(get_dispatcher_paths())
diff --git a/lava/helper/template.py b/lava/helper/template.py
new file mode 100644
index 0000000..2842b26
--- /dev/null
+++ b/lava/helper/template.py
@@ -0,0 +1,124 @@
+# Copyright (C) 2013 Linaro Limited
+#
+# Author: Milo Casagrande <milo.casagrande@linaro.org>
+#
+# This file is part of lava-tool.
+#
+# lava-tool is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License version 3
+# as published by the Free Software Foundation
+#
+# lava-tool is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with lava-tool. If not, see <http://www.gnu.org/licenses/>.
+
+"""Helper functions for a template."""
+
+from lava.parameter import Parameter
+
+
+def expand_template(template, config):
+ """Updates a template based on the values from the provided config.
+
+ :param template: A template to be updated.
+ :param config: A Config instance where values should be taken.
+ """
+
+ def update(data):
+ """Internal recursive function."""
+ if isinstance(data, dict):
+ keys = data.keys()
+ elif isinstance(data, list):
+ keys = range(len(data))
+ else:
+ return
+ for key in keys:
+ entry = data[key]
+ if isinstance(entry, Parameter):
+ data[key] = config.get(entry)
+ else:
+ update(entry)
+
+ update(template)
+
+
+def get_key(data, search_key):
+ """Goes through a template looking for a key.
+
+ :param data: The template to traverse.
+ :param search_key: The key to look for.
+ :return The key value.
+ """
+ return_value = None
+ found = False
+
+ if isinstance(data, dict):
+ bucket = []
+
+ for key, value in data.iteritems():
+ if key == search_key:
+ return_value = value
+ found = True
+ break
+ else:
+ bucket.append(value)
+
+ if bucket and not found:
+ for element in bucket:
+ if isinstance(element, list):
+ for element in element:
+ bucket.append(element)
+ elif isinstance(element, dict):
+ for key, value in element.iteritems():
+ if key == search_key:
+ return_value = value
+ found = True
+ break
+ else:
+ bucket.append(value)
+ if found:
+ break
+
+ return return_value
+
+
+def set_value(data, search_key, new_value):
+ """Sets a new value for a template key.
+
+ :param data: The data structure to update.
+ :type dict
+ :param search_key: The key to search and update.
+ :param new_value: The new value to set.
+ """
+ is_set = False
+
+ if isinstance(data, dict):
+ bucket = []
+
+ for key, value in data.iteritems():
+ if key == search_key:
+ data[key] = new_value
+ is_set = True
+ break
+ else:
+ bucket.append(value)
+
+ if bucket and not is_set:
+ for element in bucket:
+ if isinstance(element, list):
+ for element in element:
+ bucket.append(element)
+ elif isinstance(element, dict):
+ for key, value in element.iteritems():
+ if key == search_key:
+ element[key] = new_value
+ is_set = True
+ break
+ else:
+ bucket.append(value)
+ if is_set:
+ break
diff --git a/lava/helper/tests/__init__.py b/lava/helper/tests/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/lava/helper/tests/__init__.py
diff --git a/lava/helper/tests/helper_test.py b/lava/helper/tests/helper_test.py
new file mode 100644
index 0000000..039b7e2
--- /dev/null
+++ b/lava/helper/tests/helper_test.py
@@ -0,0 +1,81 @@
+# Copyright (C) 2013 Linaro Limited
+#
+# Author: Milo Casagrande <milo.casagrande@linaro.org>
+#
+# This file is part of lava-tool.
+#
+# lava-tool is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License version 3
+# as published by the Free Software Foundation
+#
+# lava-tool is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with lava-tool. If not, see <http://www.gnu.org/licenses/>.
+
+"""
+A test helper class.
+
+Here we define a general test class and its own setUp and tearDown methods that
+all other test classes can inherit from.
+"""
+
+import os
+import shutil
+import sys
+import tempfile
+
+from unittest import TestCase
+from mock import (
+ MagicMock,
+ patch
+)
+
+
+class HelperTest(TestCase):
+ """Helper test class that all tests under the lava package can inherit."""
+
+ def setUp(self):
+ # Need to patch it here, not as a decorator, or running the tests
+ # via `./setup.py test` will fail.
+ self.at_exit_patcher = patch("lava.config.AT_EXIT_CALLS", spec=set)
+ self.at_exit_patcher.start()
+ self.original_stdout = sys.stdout
+ sys.stdout = open("/dev/null", "w")
+ self.original_stderr = sys.stderr
+ sys.stderr = open("/dev/null", "w")
+ self.original_stdin = sys.stdin
+
+ self.device = "a_fake_panda02"
+
+ self.temp_file = tempfile.NamedTemporaryFile(delete=False)
+ self.temp_dir = tempfile.mkdtemp()
+ self.parser = MagicMock()
+ self.args = MagicMock()
+ self.args.interactive = MagicMock(return_value=False)
+ self.args.DEVICE = self.device
+
+ def tearDown(self):
+ self.at_exit_patcher.stop()
+ sys.stdin = self.original_stdin
+ sys.stdout = self.original_stdout
+ sys.stderr = self.original_stderr
+ shutil.rmtree(self.temp_dir)
+ os.unlink(self.temp_file.name)
+
+ def tmp(self, name):
+ """
+ Returns the full path to a file, or directory, called `name` in a
+ temporary directory.
+
+ This method does not create the file, it only gives a full filename
+ where you can actually write some data. The file will not be removed
+ by this method.
+
+ :param name: The name the file/directory should have.
+ :return A path.
+ """
+ return os.path.join(tempfile.gettempdir(), name)
diff --git a/lava/helper/tests/test_command.py b/lava/helper/tests/test_command.py
new file mode 100644
index 0000000..be2dbe6
--- /dev/null
+++ b/lava/helper/tests/test_command.py
@@ -0,0 +1,47 @@
+# Copyright (C) 2013 Linaro Limited
+#
+# Author: Milo Casagrande <milo.casagrande@linaro.org>
+#
+# This file is part of lava-tool.
+#
+# lava-tool is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License version 3
+# as published by the Free Software Foundation
+#
+# lava-tool is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with lava-tool. If not, see <http://www.gnu.org/licenses/>.
+
+"""lava.helper.command module tests."""
+
+from mock import MagicMock, patch
+
+
+from lava.helper.command import BaseCommand
+from lava.helper.tests.helper_test import HelperTest
+
+
+class BaseCommandTests(HelperTest):
+
+ def test_register_argument(self):
+ # Make sure that the parser add_argument is called and we have the
+ # correct argument.
+ command = BaseCommand(self.parser, self.args)
+ command.register_arguments(self.parser)
+ name, args, kwargs = self.parser.method_calls[0]
+ self.assertIn("--non-interactive", args)
+
+ @patch("lava.helper.command.AuthenticatingServerProxy", create=True)
+ def test_authenticated_server(self, mocked_auth_server):
+ command = BaseCommand(self.parser, self.args)
+ command.config = MagicMock()
+ command.config.get = MagicMock()
+ command.config.get.side_effect = ["www.example.org", "RPC"]
+
+ command.authenticated_server()
+
+ self.assertTrue(mocked_auth_server.called)
diff --git a/lava/helper/tests/test_dispatcher.py b/lava/helper/tests/test_dispatcher.py
new file mode 100644
index 0000000..1eb1b3a
--- /dev/null
+++ b/lava/helper/tests/test_dispatcher.py
@@ -0,0 +1,77 @@
+# Copyright (C) 2013 Linaro Limited
+#
+# Author: Milo Casagrande <milo.casagrande@linaro.org>
+#
+# This file is part of lava-tool.
+#
+# lava-tool is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License version 3
+# as published by the Free Software Foundation
+#
+# lava-tool is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with lava-tool. If not, see <http://www.gnu.org/licenses/>.
+
+"""lava.helper.dispatcher tests."""
+
+import os
+import tempfile
+
+from mock import patch
+
+from lava.tool.errors import CommandError
+from lava.helper.tests.helper_test import HelperTest
+from lava.helper.dispatcher import (
+ choose_devices_path,
+)
+
+
+class DispatcherTests(HelperTest):
+
+ def setUp(self):
+ super(DispatcherTests, self).setUp()
+ self.devices_dir = os.path.join(tempfile.gettempdir(), "devices")
+ os.makedirs(self.devices_dir)
+
+ def tearDown(self):
+ super(DispatcherTests, self).tearDown()
+ os.removedirs(self.devices_dir)
+
+ def test_choose_devices_path_0(self):
+ # Tests that when passing more than one path, the first writable one
+ # is returned.
+ obtained = choose_devices_path(
+ ["/", "/root", self.temp_dir, os.path.expanduser("~")])
+ expected = os.path.join(self.temp_dir, "devices")
+ self.assertEqual(expected, obtained)
+
+ def test_choose_devices_path_1(self):
+ # Tests that when passing a path that is not writable, CommandError
+ # is raised.
+ self.assertRaises(CommandError, choose_devices_path,
+ ["/", "/root", "/root/tmpdir"])
+
+ def test_choose_devices_path_2(self):
+ # Tests that the correct path for devices is created on the filesystem.
+ expected_path = os.path.join(self.temp_dir, "devices")
+ obtained = choose_devices_path([self.temp_dir])
+ self.assertEqual(expected_path, obtained)
+ self.assertTrue(os.path.isdir(expected_path))
+
+ def test_choose_devices_path_3(self):
+ # Tests that returns the already existing devices path.
+ obtained = choose_devices_path([tempfile.gettempdir()])
+ self.assertEqual(self.devices_dir, obtained)
+
+ @patch("__builtin__.open")
+ def test_choose_devices_path_4(self, mocked_open):
+ # Tests that when IOError is raised and we pass only one dir
+ # CommandError is raised.
+ mocked_open.side_effect = IOError()
+ self.assertRaises(CommandError, choose_devices_path,
+ [tempfile.gettempdir()])
+ self.assertTrue(mocked_open.called)
diff --git a/lava/helper/tests/test_template.py b/lava/helper/tests/test_template.py
new file mode 100644
index 0000000..f7441ca
--- /dev/null
+++ b/lava/helper/tests/test_template.py
@@ -0,0 +1,102 @@
+# Copyright (C) 2013 Linaro Limited
+#
+# Author: Milo Casagrande <milo.casagrande@linaro.org>
+#
+# This file is part of lava-tool.
+#
+# lava-tool is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License version 3
+# as published by the Free Software Foundation
+#
+# lava-tool is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with lava-tool. If not, see <http://www.gnu.org/licenses/>.
+
+""" """
+
+import copy
+from unittest import TestCase
+
+from lava.helper.template import (
+ get_key,
+ set_value
+)
+
+
+TEST_TEMPLATE = {
+ "key1": "value1",
+ "key2": [
+ "value2", "value3"
+ ],
+ "key3": [
+ {
+ "key4": "value4",
+ "key5": "value5"
+ },
+ {
+ "key6": "value6",
+ "key7": "value7"
+ },
+ [
+ {
+ "key8": "value8"
+ }
+ ]
+ ],
+ "key10": {
+ "key11": "value11"
+ }
+}
+
+
+class TestParameter(TestCase):
+
+ def test_get_key_simple_key(self):
+ expected = "value1"
+ obtained = get_key(TEST_TEMPLATE, "key1")
+ self.assertEquals(expected, obtained)
+
+ def test_get_key_nested_key(self):
+ expected = "value4"
+ obtained = get_key(TEST_TEMPLATE, "key4")
+ self.assertEquals(expected, obtained)
+
+ def test_get_key_nested_key_1(self):
+ expected = "value7"
+ obtained = get_key(TEST_TEMPLATE, "key7")
+ self.assertEquals(expected, obtained)
+
+ def test_get_key_nested_key_2(self):
+ expected = "value8"
+ obtained = get_key(TEST_TEMPLATE, "key8")
+ self.assertEquals(expected, obtained)
+
+ def test_get_key_nested_key_3(self):
+ expected = "value11"
+ obtained = get_key(TEST_TEMPLATE, "key11")
+ self.assertEquals(expected, obtained)
+
+ def test_set_value_0(self):
+ data = copy.deepcopy(TEST_TEMPLATE)
+ expected = "foo"
+ set_value(data, "key1", expected)
+ obtained = get_key(data, "key1")
+ self.assertEquals(expected, obtained)
+
+ def test_set_value_1(self):
+ data = copy.deepcopy(TEST_TEMPLATE)
+ expected = "foo"
+ set_value(data, "key6", expected)
+ obtained = get_key(data, "key6")
+ self.assertEquals(expected, obtained)
+
+ def test_set_value_2(self):
+ data = copy.deepcopy(TEST_TEMPLATE)
+ expected = "foo"
+ set_value(data, "key11", expected)
+ obtained = get_key(data, "key11")
+ self.assertEquals(expected, obtained)
diff --git a/lava/job/__init__.py b/lava/job/__init__.py
new file mode 100644
index 0000000..e959f1d
--- /dev/null
+++ b/lava/job/__init__.py
@@ -0,0 +1,73 @@
+# Copyright (C) 2013 Linaro Limited
+#
+# Author: Antonio Terceiro <antonio.terceiro@linaro.org>
+#
+# This file is part of lava-tool.
+#
+# lava-tool is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License version 3
+# as published by the Free Software Foundation
+#
+# lava-tool is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with lava-tool. If not, see <http://www.gnu.org/licenses/>.
+
+"""Job class."""
+
+import json
+
+from copy import deepcopy
+
+from lava.helper.template import (
+ expand_template,
+ set_value,
+)
+from lava_tool.utils import (
+ verify_file_extension,
+ verify_path_existance,
+ write_file
+)
+
+# A default name for job files.
+DEFAULT_JOB_FILENAME = "lava-tool-job.json"
+# Default job file extension.
+DEFAULT_JOB_EXTENSION = "json"
+# Possible extension for a job file.
+JOB_FILE_EXTENSIONS = [DEFAULT_JOB_EXTENSION]
+
+
+class Job(object):
+
+ """A Job object.
+
+ This class should be used to create new job files. The initialization
+ enforces a default file name extension, and makes sure that the file is
+ not already present on the file system.
+ """
+
+ def __init__(self, data, file_name):
+ self.file_name = verify_file_extension(file_name,
+ DEFAULT_JOB_EXTENSION,
+ JOB_FILE_EXTENSIONS)
+ verify_path_existance(self.file_name)
+ self.data = deepcopy(data)
+
+ def set(self, key, value):
+ """Set key to the specified value.
+
+ :param key: The key to look in the object data.
+ :param value: The value to set.
+ """
+ set_value(self.data, key, value)
+
+ def update(self, config):
+ """Updates the Job object based on the provided config."""
+ expand_template(self.data, config)
+
+ def write(self):
+ """Writes the Job object to file."""
+ write_file(self.file_name, json.dumps(self.data, indent=4))
diff --git a/lava/job/commands.py b/lava/job/commands.py
new file mode 100644
index 0000000..9535320
--- /dev/null
+++ b/lava/job/commands.py
@@ -0,0 +1,107 @@
+# Copyright (C) 2013 Linaro Limited
+#
+# Author: Antonio Terceiro <antonio.terceiro@linaro.org>
+#
+# This file is part of lava-tool.
+#
+# lava-tool is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License version 3
+# as published by the Free Software Foundation
+#
+# lava-tool is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with lava-tool. If not, see <http://www.gnu.org/licenses/>.
+
+"""
+LAVA job commands.
+"""
+
+import os
+
+from lava.helper.command import BaseCommand
+from lava.job import Job
+from lava.job.templates import (
+ BOOT_TEST_KEY,
+ JOB_TYPES,
+)
+from lava.tool.command import CommandGroup
+from lava.tool.errors import CommandError
+
+
+class job(CommandGroup):
+ """LAVA job file handling."""
+ namespace = 'lava.job.commands'
+
+
+class new(BaseCommand):
+ """Creates a new job file."""
+
+ @classmethod
+ def register_arguments(cls, parser):
+ super(new, cls).register_arguments(parser)
+ parser.add_argument("FILE", help=("Job file to be created."))
+ parser.add_argument("--type",
+ help=("The type of job to create. Defaults to "
+ "'{0}'.".format(BOOT_TEST_KEY)),
+ choices=JOB_TYPES.keys(),
+ default=BOOT_TEST_KEY)
+
+ def invoke(self, job_template=None):
+ if not job_template:
+ job_template = JOB_TYPES.get(self.args.type)
+
+ full_path = os.path.abspath(self.args.FILE)
+
+ job_instance = Job(job_template, full_path)
+ job_instance.update(self.config)
+ job_instance.write()
+
+
+class submit(BaseCommand):
+
+ """Submits the specified job file."""
+
+ @classmethod
+ def register_arguments(cls, parser):
+ super(submit, cls).register_arguments(parser)
+ parser.add_argument("FILE", help=("The job file to submit."))
+
+ def invoke(self):
+ super(submit, self).submit(self.args.FILE)
+
+
+class run(BaseCommand):
+
+ """Runs the specified job file on the local dispatcher."""
+
+ @classmethod
+ def register_arguments(cls, parser):
+ super(run, cls).register_arguments(parser)
+ parser.add_argument("FILE", help=("The job file to submit."))
+
+ def invoke(self):
+ super(run, self).run(self.args.FILE)
+
+
+class status(BaseCommand):
+
+ """Retrieves the status of a job."""
+
+ @classmethod
+ def register_arguments(cls, parser):
+ super(status, cls).register_arguments(parser)
+ parser.add_argument("JOB_ID",
+ help=("Prints status information about the "
+ "provided job id."),
+ nargs="?",
+ default=None)
+
+ def invoke(self):
+ if self.args.JOB_ID:
+ super(status, self).status(self.args.JOB_ID)
+ else:
+ raise CommandError("It is necessary to specify a job id.")
diff --git a/lava/job/templates.py b/lava/job/templates.py
new file mode 100644
index 0000000..e50180e
--- /dev/null
+++ b/lava/job/templates.py
@@ -0,0 +1,106 @@
+# Copyright (C) 2013 Linaro Limited
+#
+# Author: Antonio Terceiro <antonio.terceiro@linaro.org>
+#
+# This file is part of lava-tool.
+#
+# lava-tool is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License version 3
+# as published by the Free Software Foundation
+#
+# lava-tool is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with lava-tool. If not, see <http://www.gnu.org/licenses/>.
+
+from lava.parameter import (
+ ListParameter,
+ Parameter,
+)
+
+LAVA_TEST_SHELL_TAR_REPO_KEY = "tar-repo"
+LAVA_TEST_SHELL_TESDEF_KEY = "testdef"
+
+DEVICE_TYPE_PARAMETER = Parameter("device_type")
+PREBUILT_IMAGE_PARAMETER = Parameter("image", depends=DEVICE_TYPE_PARAMETER)
+
+TESTDEF_URLS_PARAMETER = ListParameter("testdef_urls")
+TESTDEF_URLS_PARAMETER.store = False
+
+BOOT_TEST = {
+ "timeout": 18000,
+ "job_name": "Boot test",
+ "device_type": DEVICE_TYPE_PARAMETER,
+ "actions": [
+ {
+ "command": "deploy_linaro_image",
+ "parameters": {
+ "image": PREBUILT_IMAGE_PARAMETER
+ }
+ },
+ {
+ "command": "boot_linaro_image"
+ }
+ ]
+}
+
+LAVA_TEST_SHELL = {
+ "job_name": "LAVA Test Shell",
+ "timeout": 18000,
+ "device_type": DEVICE_TYPE_PARAMETER,
+ "actions": [
+ {
+ "command": "deploy_linaro_image",
+ "parameters": {
+ "image": PREBUILT_IMAGE_PARAMETER,
+ }
+ },
+ {
+ "command": "lava_test_shell",
+ "parameters": {
+ "timeout": 1800,
+ "testdef_urls": TESTDEF_URLS_PARAMETER,
+ }
+ }
+ ]
+}
+
+# This is a special case template, only use when automatically create job files
+# starting from a testdef or a script. Never to be used directly by the user.
+LAVA_TEST_SHELL_TAR_REPO = {
+ "job_name": "LAVA Test Shell",
+ "timeout": 18000,
+ "device_type": DEVICE_TYPE_PARAMETER,
+ "actions": [
+ {
+ "command": "deploy_linaro_image",
+ "parameters": {
+ "image": PREBUILT_IMAGE_PARAMETER,
+ }
+ },
+ {
+ "command": "lava_test_shell",
+ "parameters": {
+ "timeout": 1800,
+ "testdef_repos": [
+ {
+ LAVA_TEST_SHELL_TESDEF_KEY: None,
+ LAVA_TEST_SHELL_TAR_REPO_KEY: None,
+ }
+ ]
+ }
+ }
+ ]
+}
+
+BOOT_TEST_KEY = "boot-test"
+LAVA_TEST_SHELL_KEY = "lava-test-shell"
+
+# Dict with all the user available job templates.
+JOB_TYPES = {
+ BOOT_TEST_KEY: BOOT_TEST,
+ LAVA_TEST_SHELL_KEY: LAVA_TEST_SHELL,
+}
diff --git a/lava/job/tests/__init__.py b/lava/job/tests/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/lava/job/tests/__init__.py
diff --git a/lava/job/tests/test_commands.py b/lava/job/tests/test_commands.py
new file mode 100644
index 0000000..79f352f
--- /dev/null
+++ b/lava/job/tests/test_commands.py
@@ -0,0 +1,155 @@
+# Copyright (C) 2013 Linaro Limited
+#
+# Author: Antonio Terceiro <antonio.terceiro@linaro.org>
+#
+# This file is part of lava-tool.
+#
+# lava-tool is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License version 3
+# as published by the Free Software Foundation
+#
+# lava-tool is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with lava-tool. If not, see <http://www.gnu.org/licenses/>.
+
+"""
+Unit tests for the commands classes
+"""
+
+import json
+import os
+
+from mock import patch
+
+from lava.config import Config
+from lava.helper.tests.helper_test import HelperTest
+from lava.job.commands import (
+ new,
+ run,
+ submit,
+ status,
+)
+from lava.parameter import Parameter
+from lava.tool.errors import CommandError
+
+
+class CommandTest(HelperTest):
+
+ def setUp(self):
+ super(CommandTest, self).setUp()
+ self.args.FILE = self.temp_file.name
+ self.args.type = "boot-test"
+
+ self.device_type = Parameter('device_type')
+ self.prebuilt_image = Parameter('prebuilt_image',
+ depends=self.device_type)
+ self.config = Config()
+ self.config.put_parameter(self.device_type, 'foo')
+ self.config.put_parameter(self.prebuilt_image, 'bar')
+
+
+class JobNewTest(CommandTest):
+
+ def setUp(self):
+ super(JobNewTest, self).setUp()
+ self.args.FILE = self.tmp("new_file.json")
+ self.new_command = new(self.parser, self.args)
+ self.new_command.config = self.config
+
+ def tearDown(self):
+ super(JobNewTest, self).tearDown()
+ if os.path.exists(self.args.FILE):
+ os.unlink(self.args.FILE)
+
+ def test_register_arguments(self):
+ new_cmd = new(self.parser, self.args)
+ new_cmd.register_arguments(self.parser)
+
+ # Make sure we do not forget about this test.
+ self.assertEqual(3, len(self.parser.method_calls))
+
+ _, args, _ = self.parser.method_calls[0]
+ self.assertIn("--non-interactive", args)
+
+ _, args, _ = self.parser.method_calls[1]
+ self.assertIn("FILE", args)
+
+ _, args, _ = self.parser.method_calls[2]
+ self.assertIn("--type", args)
+
+ def test_create_new_file(self):
+ self.new_command.invoke()
+ self.assertTrue(os.path.exists(self.args.FILE))
+
+ def test_fills_in_template_parameters(self):
+ self.new_command.invoke()
+
+ data = json.loads(open(self.args.FILE).read())
+ self.assertEqual(data['device_type'], 'foo')
+
+ def test_wont_overwrite_existing_file(self):
+ with open(self.args.FILE, 'w') as f:
+ f.write("CONTENTS")
+
+ self.assertRaises(CommandError, self.new_command.invoke)
+ self.assertEqual("CONTENTS", open(self.args.FILE).read())
+
+
+class JobSubmitTest(CommandTest):
+
+ def test_receives_job_file_in_cmdline(self):
+ command = submit(self.parser, self.args)
+ command.register_arguments(self.parser)
+ name, args, kwargs = self.parser.method_calls[1]
+ self.assertIn("FILE", args)
+
+
+class JobRunTest(CommandTest):
+
+ def test_register_arguments(self):
+ run_cmd = run(self.parser, self.args)
+ run_cmd.register_arguments(self.parser)
+
+ # Make sure we do not forget about this test.
+ self.assertEqual(2, len(self.parser.method_calls))
+
+ _, args, _ = self.parser.method_calls[0]
+ self.assertIn("--non-interactive", args)
+
+ _, args, _ = self.parser.method_calls[1]
+ self.assertIn("FILE", args)
+
+ def test_invoke_raises_0(self):
+ # Users passes a non existing job file to the run command.
+ self.args.FILE = self.tmp("test_invoke_raises_0.json")
+ command = run(self.parser, self.args)
+ self.assertRaises(CommandError, command.invoke)
+
+ @patch("lava.helper.command.has_command", create=True)
+ def test_invoke_raises_1(self, mocked_has_command):
+ # User passes a valid file to the run command, but she does not have
+ # the dispatcher installed.
+ mocked_has_command.return_value = False
+ command = run(self.parser, self.args)
+ self.assertRaises(CommandError, command.invoke)
+
+
+class TestsStatusCommand(CommandTest):
+
+ def test_register_arguments(self):
+ self.args.JOB_ID = "1"
+ status_cmd = status(self.parser, self.args)
+ status_cmd.register_arguments(self.parser)
+
+ # Make sure we do not forget about this test.
+ self.assertEqual(2, len(self.parser.method_calls))
+
+ _, args, _ = self.parser.method_calls[0]
+ self.assertIn("--non-interactive", args)
+
+ _, args, _ = self.parser.method_calls[1]
+ self.assertIn("JOB_ID", args)
diff --git a/lava/job/tests/test_job.py b/lava/job/tests/test_job.py
new file mode 100644
index 0000000..a6df99d
--- /dev/null
+++ b/lava/job/tests/test_job.py
@@ -0,0 +1,92 @@
+# Copyright (C) 2013 Linaro Limited
+#
+# Author: Antonio Terceiro <antonio.terceiro@linaro.org>
+#
+# This file is part of lava-tool.
+#
+# lava-tool is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License version 3
+# as published by the Free Software Foundation
+#
+# lava-tool is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with lava-tool. If not, see <http://www.gnu.org/licenses/>.
+
+"""
+Unit tests for the Job class
+"""
+
+import os
+import json
+import tempfile
+
+from mock import patch
+
+from lava.config import Config
+from lava.helper.tests.helper_test import HelperTest
+from lava.job import Job
+from lava.job.templates import BOOT_TEST
+from lava.parameter import Parameter
+
+
+class JobTest(HelperTest):
+
+ @patch("lava.config.Config.save")
+ def setUp(self, mocked_config):
+ super(JobTest, self).setUp()
+ self.config = Config()
+ self.config.config_file = self.temp_file.name
+
+ def test_from_template(self):
+ template = {}
+ job = Job(template, self.temp_file.name)
+ self.assertEqual(job.data, template)
+ self.assertIsNot(job.data, template)
+
+ def test_update_data(self):
+ image = "/path/to/panda.img"
+ param1 = Parameter("device_type")
+ param2 = Parameter("image", depends=param1)
+ self.config.put_parameter(param1, "panda")
+ self.config.put_parameter(param2, image)
+
+ job = Job(BOOT_TEST, self.temp_file.name)
+ job.update(self.config)
+
+ self.assertEqual(job.data['device_type'], "panda")
+ self.assertEqual(job.data['actions'][0]["parameters"]["image"], image)
+
+ def test_write(self):
+ try:
+ orig_data = {"foo": "bar"}
+ job_file = os.path.join(tempfile.gettempdir(), "a_json_file.json")
+ job = Job(orig_data, job_file)
+ job.write()
+
+ output = ""
+ with open(job_file) as read_file:
+ output = read_file.read()
+
+ data = json.loads(output)
+ self.assertEqual(data, orig_data)
+ finally:
+ os.unlink(job_file)
+
+ def test_writes_nicely_formatted_json(self):
+ try:
+ orig_data = {"foo": "bar"}
+ job_file = os.path.join(tempfile.gettempdir(), "b_json_file.json")
+ job = Job(orig_data, job_file)
+ job.write()
+
+ output = ""
+ with open(job_file) as read_file:
+ output = read_file.read()
+
+ self.assertTrue(output.startswith("{\n"))
+ finally:
+ os.unlink(job_file)
diff --git a/lava/parameter.py b/lava/parameter.py
new file mode 100644
index 0000000..dfb0883
--- /dev/null
+++ b/lava/parameter.py
@@ -0,0 +1,256 @@
+# Copyright (C) 2013 Linaro Limited
+#
+# Author: Antonio Terceiro <antonio.terceiro@linaro.org>
+#
+# This file is part of lava-tool.
+#
+# lava-tool is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License version 3
+# as published by the Free Software Foundation
+#
+# lava-tool is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with lava-tool. If not, see <http://www.gnu.org/licenses/>.
+
+"""
+Parameter class and its accessory methods/functions.
+"""
+
+import sys
+import types
+
+from lava_tool.utils import to_list
+
+# Character used to join serialized list parameters.
+LIST_SERIALIZE_DELIMITER = ","
+
+
+class Parameter(object):
+
+ """A parameter with an optional dependency."""
+
+ def __init__(self, id, value=None, depends=None):
+ """Creates a new parameter.
+
+ :param id: The name of this parameter.
+ :param value: The value of this parameter. Defaults to None.
+ :param depends: If this Parameter depends on another one. Defaults
+ to None.
+ :type Parameter
+ """
+ self.id = id
+ self.value = value
+ self.depends = depends
+ self.asked = False
+ # Whether to store or not the parameter in the user config file.
+ self.store = True
+
+ def set(self, value):
+ """Sets the value of the parameter.
+
+ :param value: The value to set.
+ """
+ self.value = value
+
+ def prompt(self, old_value=None):
+ """Gets the parameter value from the user.
+
+ To get user input, the builtin `raw_input` function will be used. Input
+ will also be stripped of possible whitespace chars. If Enter or any
+ sort of whitespace chars in typed, the old Parameter value will be
+ returned.
+
+ :param old_value: The old parameter value.
+ :return The input as typed by the user, or the old value.
+ """
+ if not self.asked:
+ if old_value is not None:
+ prompt = "{0} [{1}]: ".format(self.id, old_value)
+ else:
+ prompt = "{0}: ".format(self.id)
+
+ user_input = self.get_user_input(prompt)
+
+ if user_input is not None:
+ if len(user_input) == 0 and old_value:
+ # Keep the old value when user press enter or another
+ # whitespace char.
+ self.value = old_value
+ else:
+ self.value = user_input
+
+ self.asked = True
+
+ return self.value
+
+ @classmethod
+ def get_user_input(cls, prompt=""):
+ """Asks the user for input data.
+
+ :param prompt: The prompt that should be given to the user.
+ :return A string with what the user typed.
+ """
+ data = None
+ try:
+ data = raw_input(prompt).strip()
+ except EOFError:
+ # Force to return None.
+ data = None
+ except KeyboardInterrupt:
+ sys.exit(-1)
+ return data
+
+ @classmethod
+ def serialize(cls, value):
+ """Serializes the passed value to be friendly written to file.
+
+ Lists are serialized as a comma separated string of values.
+
+ :param value: The value to serialize.
+ :return The serialized value as string.
+ """
+ serialized = ""
+ if isinstance(value, list):
+ serialized = LIST_SERIALIZE_DELIMITER.join(
+ str(x) for x in value if x)
+ else:
+ serialized = str(value)
+ return serialized
+
+ @classmethod
+ def deserialize(cls, value):
+ """Deserialize a value into a list.
+
+ The value must have been serialized with the class instance serialize()
+ method.
+
+ :param value: The string value to be deserialized.
+ :type str
+ :return A list of values.
+ """
+ deserialized = []
+ if isinstance(value, str):
+ deserialized = filter(None, (x.strip() for x in value.split(
+ LIST_SERIALIZE_DELIMITER)))
+ else:
+ deserialized = list(value)
+ return deserialized
+
+
+class SingleChoiceParameter(Parameter):
+
+ """A parameter implemeting a single choice between multiple choices."""
+
+ def __init__(self, id, choices):
+ super(SingleChoiceParameter, self).__init__(id)
+ self.choices = to_list(choices)
+
+ def prompt(self, prompt, old_value=None):
+ """Asks the user for their choice."""
+ # Sliglty different than the other parameters: here we first present
+ # the user with what the choices are about.
+ print >> sys.stdout, prompt
+
+ index = 1
+ for choice in self.choices:
+ print >> sys.stdout, "\t{0:d}. {1}".format(index, choice)
+ index += 1
+
+ choices_len = len(self.choices)
+ while True:
+ user_input = self.get_user_input("Choice: ")
+
+ if len(user_input) == 0 and old_value:
+ choice = old_value
+ break
+ elif user_input in [str(x) for x in range(1, choices_len + 1)]:
+ choice = self.choices[int(user_input) - 1]
+ break
+
+ return choice
+
+
+class ListParameter(Parameter):
+
+ """A specialized Parameter to handle list values."""
+
+ # This is used as a deletion character. When we have an old value and the
+ # user enters this char, it sort of deletes the value.
+ DELETE_CHAR = "-"
+
+ def __init__(self, id, value=None, depends=None):
+ super(ListParameter, self).__init__(id, depends=depends)
+ self.value = []
+ if value:
+ self.set(value)
+
+ def set(self, value):
+ """Sets the value of the parameter.
+
+ :param value: The value to set.
+ """
+ self.value = to_list(value)
+
+ def add(self, value):
+ """Adds a new value to the list of values of this parameter.
+
+ :param value: The value to add.
+ """
+ if isinstance(value, list):
+ self.value.extend(value)
+ else:
+ self.value.append(value)
+
+ def prompt(self, old_value=None):
+ """Gets the parameter in a list form.
+
+ To exit the input procedure it is necessary to insert an empty line.
+
+ :return The list of values.
+ """
+
+ if not self.asked:
+ if old_value is not None:
+ # We might get the old value read from file via ConfigParser,
+ # and usually it comes in string format.
+ old_value = self.deserialize(old_value)
+
+ print >> sys.stdout, "Values for '{0}': ".format(self.id)
+
+ index = 1
+ while True:
+ user_input = None
+ if old_value is not None and (0 < len(old_value) >= index):
+ prompt = "{0:>3d}.\n\told: {1}\n\tnew: ".format(
+ index, old_value[index - 1])
+ user_input = self.get_user_input(prompt)
+ else:
+ prompt = "{0:>3d}. ".format(index)
+ user_input = self.get_user_input(prompt)
+
+ if user_input is not None:
+ # The user has pressed Enter.
+ if len(user_input) == 0:
+ if old_value is not None and \
+ (0 < len(old_value) >= index):
+ user_input = old_value[index - 1]
+ else:
+ break
+
+ if len(user_input) == 1 and user_input == \
+ self.DELETE_CHAR and (0 < len(old_value) >= index):
+ # We have an old value, user presses the DELETE_CHAR
+ # and we do not store anything. This is done to delete
+ # an old entry.
+ pass
+ else:
+ self.value.append(user_input)
+ index += 1
+
+ self.asked = True
+
+ return self.value
diff --git a/lava/script/__init__.py b/lava/script/__init__.py
new file mode 100644
index 0000000..e70c5d0
--- /dev/null
+++ b/lava/script/__init__.py
@@ -0,0 +1,51 @@
+# Copyright (C) 2013 Linaro Limited
+#
+# Author: Milo Casagrande <milo.casagrande@linaro.org>
+#
+# This file is part of lava-tool.
+#
+# lava-tool is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License version 3
+# as published by the Free Software Foundation
+#
+# lava-tool is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with lava-tool. If not, see <http://www.gnu.org/licenses/>.
+
+"""Scripts handling class."""
+
+import os
+import stat
+
+from lava_tool.utils import write_file
+
+
+DEFAULT_MOD = stat.S_IRWXU | stat.S_IRWXG | stat.S_IROTH | stat.S_IXOTH
+DEFAULT_TESTDEF_SCRIPT_CONTENT = """#!/bin/sh
+# Automatic generated content by lava-tool.
+# Please add your own instructions.
+#
+# You can use all the avialable Bash commands.
+#
+# For the available LAVA commands, see:
+# http://lava.readthedocs.org/
+#
+"""
+DEFAULT_TESTDEF_SCRIPT = "mytest.sh"
+
+
+class ShellScript(object):
+
+ """Creates a shell script on the file system with some content."""
+
+ def __init__(self, file_name):
+ self.file_name = file_name
+
+ def write(self):
+ write_file(self.file_name, DEFAULT_TESTDEF_SCRIPT_CONTENT)
+ # Make sure the script is executable.
+ os.chmod(self.file_name, DEFAULT_MOD)
diff --git a/lava/script/commands.py b/lava/script/commands.py
new file mode 100644
index 0000000..c5e7af0
--- /dev/null
+++ b/lava/script/commands.py
@@ -0,0 +1,115 @@
+# Copyright (C) 2013 Linaro Limited
+#
+# Author: Milo Casagrande <milo.casagrande@linaro.org>
+#
+# This file is part of lava-tool.
+#
+# lava-tool is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License version 3
+# as published by the Free Software Foundation
+#
+# lava-tool is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with lava-tool. If not, see <http://www.gnu.org/licenses/>.
+
+"""Commands to run or submit a script."""
+
+import os
+import tempfile
+
+from lava.helper.command import BaseCommand
+from lava.job import DEFAULT_JOB_FILENAME
+from lava.testdef import DEFAULT_TESTDEF_FILENAME
+from lava.tool.command import CommandGroup
+from lava_tool.utils import verify_path_non_existance
+
+
+class script(CommandGroup):
+
+ """LAVA script file handling."""
+
+ namespace = "lava.script.commands"
+
+
+class ScriptBaseCommand(BaseCommand):
+
+ def _create_tmp_job_file(self, script_file):
+ """Creates a temporary job file to run or submit the passed file.
+
+ The temporary job file and its accessory test definition file are
+ not removed by this method.
+
+ :param script_file: The script file that has to be run or submitted.
+ :return A tuple with the job file path, and the test definition path.
+ """
+ script_file = os.path.abspath(script_file)
+ verify_path_non_existance(script_file)
+
+ temp_dir = tempfile.gettempdir()
+
+ # The name of the job and testdef files.
+ job_file = os.path.join(temp_dir, DEFAULT_JOB_FILENAME)
+ testdef_file = os.path.join(temp_dir, DEFAULT_TESTDEF_FILENAME)
+
+ # The steps that the testdef file should have. We need to change it
+ # from the default one, since the users are passing their own file.
+ steps = "./" + os.path.basename(script_file)
+ testdef_file = self.create_test_definition(testdef_file,
+ steps=steps)
+
+ # The content of the tar file.
+ tar_content = [script_file, testdef_file]
+ job_file = self.create_tar_repo_job(job_file, testdef_file,
+ tar_content)
+
+ return (job_file, testdef_file)
+
+
+class run(ScriptBaseCommand):
+
+ """Runs the specified shell script on a local device."""
+
+ @classmethod
+ def register_arguments(cls, parser):
+ super(run, cls).register_arguments(parser)
+ parser.add_argument("FILE", help="Shell script file to run.")
+
+ def invoke(self):
+ job_file = ""
+ testdef_file = ""
+
+ try:
+ job_file, testdef_file = self._create_tmp_job_file(self.args.FILE)
+ super(run, self).run(job_file)
+ finally:
+ if os.path.isfile(job_file):
+ os.unlink(job_file)
+ if os.path.isfile(testdef_file):
+ os.unlink(testdef_file)
+
+
+class submit(ScriptBaseCommand):
+
+ """Submits the specified shell script to a LAVA server."""
+
+ @classmethod
+ def register_arguments(cls, parser):
+ super(submit, cls).register_arguments(parser)
+ parser.add_argument("FILE", help="Shell script file to send.")
+
+ def invoke(self):
+ job_file = ""
+ testdef_file = ""
+
+ try:
+ job_file, testdef_file = self._create_tmp_job_file(self.args.FILE)
+ super(submit, self).submit(job_file)
+ finally:
+ if os.path.isfile(job_file):
+ os.unlink(job_file)
+ if os.path.isfile(testdef_file):
+ os.unlink(testdef_file)
diff --git a/lava/script/tests/__init__.py b/lava/script/tests/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/lava/script/tests/__init__.py
diff --git a/lava/script/tests/test_commands.py b/lava/script/tests/test_commands.py
new file mode 100644
index 0000000..e237472
--- /dev/null
+++ b/lava/script/tests/test_commands.py
@@ -0,0 +1,59 @@
+# Copyright (C) 2013 Linaro Limited
+#
+# Author: Milo Casagrande <milo.casagrande@linaro.org>
+#
+# This file is part of lava-tool.
+#
+# lava-tool is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License version 3
+# as published by the Free Software Foundation
+#
+# lava-tool is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with lava-tool. If not, see <http://www.gnu.org/licenses/>.
+
+"""
+Tests for lava.script.commands.
+"""
+
+from lava.helper.tests.helper_test import HelperTest
+from lava.script.commands import (
+ run,
+ submit,
+)
+
+
+class RunCommandTests(HelperTest):
+
+ def test_register_arguments(self):
+ run_cmd = run(self.parser, self.args)
+ run_cmd.register_arguments(self.parser)
+
+ # Make sure we do not forget about this test.
+ self.assertEqual(2, len(self.parser.method_calls))
+
+ _, args, _ = self.parser.method_calls[0]
+ self.assertIn("--non-interactive", args)
+
+ _, args, _ = self.parser.method_calls[1]
+ self.assertIn("FILE", args)
+
+
+class SubmitCommandTests(HelperTest):
+
+ def test_register_arguments(self):
+ submit_cmd = submit(self.parser, self.args)
+ submit_cmd.register_arguments(self.parser)
+
+ # Make sure we do not forget about this test.
+ self.assertEqual(2, len(self.parser.method_calls))
+
+ _, args, _ = self.parser.method_calls[0]
+ self.assertIn("--non-interactive", args)
+
+ _, args, _ = self.parser.method_calls[1]
+ self.assertIn("FILE", args)
diff --git a/lava/script/tests/test_script.py b/lava/script/tests/test_script.py
new file mode 100644
index 0000000..13a800a
--- /dev/null
+++ b/lava/script/tests/test_script.py
@@ -0,0 +1,80 @@
+# Copyright (C) 2013 Linaro Limited
+#
+# Author: Milo Casagrande <milo.casagrande@linaro.org>
+#
+# This file is part of lava-tool.
+#
+# lava-tool is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License version 3
+# as published by the Free Software Foundation
+#
+# lava-tool is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with lava-tool. If not, see <http://www.gnu.org/licenses/>.
+
+"""
+Unittests for the ShellScript class.
+"""
+
+import os
+import stat
+
+from lava.helper.tests.helper_test import HelperTest
+from lava.script import ShellScript
+
+
+class ShellScriptTests(HelperTest):
+
+ """ShellScript tests."""
+
+ def test_create_file(self):
+ # Tests that a shell script is actually written.
+ try:
+ temp_file = self.tmp("a_shell_test")
+ script = ShellScript(temp_file)
+ script.write()
+
+ self.assertTrue(os.path.isfile(temp_file))
+ finally:
+ os.unlink(temp_file)
+
+ def test_assure_executable(self):
+ # Tests that the shell script created is executable.
+ try:
+ temp_file = self.tmp("a_shell_test")
+ script = ShellScript(temp_file)
+ script.write()
+
+ expected = (stat.S_IRWXU | stat.S_IRWXG | stat.S_IROTH |
+ stat.S_IXOTH)
+
+ obtained = stat.S_IMODE(os.stat(temp_file).st_mode)
+ self.assertEquals(expected, obtained)
+ finally:
+ os.unlink(temp_file)
+
+ def test_shell_script_content(self):
+ # Tests that the shell script created contains the exepcted content.
+ try:
+ temp_file = self.tmp("a_shell_test")
+ script = ShellScript(temp_file)
+ script.write()
+
+ obtained = ""
+ with open(temp_file) as read_file:
+ obtained = read_file.read()
+
+ expected = ("#!/bin/sh\n# Automatic generated "
+ "content by lava-tool.\n# Please add your own "
+ "instructions.\n#\n# You can use all the avialable "
+ "Bash commands.\n#\n# For the available LAVA "
+ "commands, see:\n# http://lava.readthedocs.org/\n"
+ "#\n")
+
+ self.assertEquals(expected, obtained)
+ finally:
+ os.unlink(temp_file)
diff --git a/lava/testdef/__init__.py b/lava/testdef/__init__.py
new file mode 100644
index 0000000..69c013f
--- /dev/null
+++ b/lava/testdef/__init__.py
@@ -0,0 +1,82 @@
+# Copyright (C) 2013 Linaro Limited
+#
+# Author: Milo Casagrande <milo.casagrande@linaro.org>
+#
+# This file is part of lava-tool.
+#
+# lava-tool is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License version 3
+# as published by the Free Software Foundation
+#
+# lava-tool is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with lava-tool. If not, see <http://www.gnu.org/licenses/>.
+
+"""TestDefinition class."""
+
+import yaml
+
+from copy import deepcopy
+
+from lava.helper.template import (
+ expand_template,
+ set_value,
+)
+from lava_tool.utils import (
+ write_file,
+ verify_path_existance,
+ verify_file_extension,
+)
+
+# Default name for a test definition.
+DEFAULT_TESTDEF_FILENAME = "lavatest.yaml"
+# Default test def file extension.
+DEFAULT_TESTDEF_EXTENSION = "yaml"
+# Possible extensions for a test def file.
+TESTDEF_FILE_EXTENSIONS = [DEFAULT_TESTDEF_EXTENSION]
+
+
+class TestDefinition(object):
+
+ """A test definition object.
+
+ This class should be used to create test definitions. The initialization
+ enforces a default file name extension, and makes sure that the file is
+ not already present on the file system.
+ """
+
+ def __init__(self, data, file_name):
+ """Initialize the object.
+
+ :param data: The serializable data to be used, usually a template.
+ :type dict
+ :param file_name: Where the test definition will be written.
+ :type str
+ """
+ self.file_name = verify_file_extension(file_name,
+ DEFAULT_TESTDEF_EXTENSION,
+ TESTDEF_FILE_EXTENSIONS)
+ verify_path_existance(self.file_name)
+
+ self.data = deepcopy(data)
+
+ def set(self, key, value):
+ """Set key to the specified value.
+
+ :param key: The key to look in the object data.
+ :param value: The value to set.
+ """
+ set_value(self.data, key, value)
+
+ def write(self):
+ """Writes the test definition to file."""
+ content = yaml.dump(self.data, default_flow_style=False, indent=4)
+ write_file(self.file_name, content)
+
+ def update(self, config):
+ """Updates the TestDefinition object based on the provided config."""
+ expand_template(self.data, config)
diff --git a/lava/testdef/commands.py b/lava/testdef/commands.py
new file mode 100644
index 0000000..87046ae
--- /dev/null
+++ b/lava/testdef/commands.py
@@ -0,0 +1,104 @@
+# Copyright (C) 2013 Linaro Limited
+#
+# Author: Milo Casagrande <milo.casagrande@linaro.org>
+#
+# This file is part of lava-tool.
+#
+# lava-tool is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License version 3
+# as published by the Free Software Foundation
+#
+# lava-tool is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with lava-tool. If not, see <http://www.gnu.org/licenses/>.
+
+"""
+Test definition commands class.
+"""
+
+import os
+import tempfile
+
+from lava.helper.command import BaseCommand
+from lava.job import DEFAULT_JOB_FILENAME
+from lava.tool.command import CommandGroup
+from lava_tool.utils import verify_path_non_existance
+
+
+class testdef(CommandGroup):
+
+ """LAVA test definitions handling."""
+
+ namespace = "lava.testdef.commands"
+
+
+class TestdefBaseCommand(BaseCommand):
+
+ def _create_tmp_job_file(self, testdef_file):
+ testdef_file = os.path.abspath(testdef_file)
+ verify_path_non_existance(testdef_file)
+
+ job_file = os.path.join(tempfile.gettempdir(),
+ DEFAULT_JOB_FILENAME)
+
+ tar_content = [testdef_file]
+ job_file = self.create_tar_repo_job(job_file, testdef_file,
+ tar_content)
+
+ return job_file
+
+
+class new(TestdefBaseCommand):
+
+ """Creates a new test definition file."""
+
+ @classmethod
+ def register_arguments(cls, parser):
+ super(new, cls).register_arguments(parser)
+ parser.add_argument("FILE", help="Test definition file to create.")
+
+ def invoke(self):
+ full_path = os.path.abspath(self.args.FILE)
+ self.create_test_definition(full_path)
+
+
+class run(TestdefBaseCommand):
+
+ """Runs the specified test definition on a local device."""
+
+ @classmethod
+ def register_arguments(cls, parser):
+ super(run, cls).register_arguments(parser)
+ parser.add_argument("FILE", help="Test definition file to run.")
+
+ def invoke(self):
+ job_file = ""
+ try:
+ job_file = self._create_tmp_job_file(self.args.FILE)
+ super(run, self).run(job_file)
+ finally:
+ if os.path.isfile(job_file):
+ os.unlink(job_file)
+
+
+class submit(TestdefBaseCommand):
+
+ """Submits the specified test definition to a LAVA server."""
+
+ @classmethod
+ def register_arguments(cls, parser):
+ super(submit, cls).register_arguments(parser)
+ parser.add_argument("FILE", help="Test definition file to send.")
+
+ def invoke(self):
+ job_file = ""
+ try:
+ job_file = self._create_tmp_job_file(self.args.FILE)
+ super(submit, self).submit(job_file)
+ finally:
+ if os.path.isfile(job_file):
+ os.unlink(job_file)
diff --git a/lava/testdef/templates.py b/lava/testdef/templates.py
new file mode 100644
index 0000000..67efea9
--- /dev/null
+++ b/lava/testdef/templates.py
@@ -0,0 +1,52 @@
+# Copyright (C) 2013 Linaro Limited
+#
+# Author: Milo Casagrande <milo.casagrande@linaro.org>
+#
+# This file is part of lava-tool.
+#
+# lava-tool is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License version 3
+# as published by the Free Software Foundation
+#
+# lava-tool is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with lava-tool. If not, see <http://www.gnu.org/licenses/>.
+
+"""Test definition templates."""
+
+from lava.parameter import (
+ Parameter,
+)
+
+DEFAULT_TESTDEF_VERSION = "1.0"
+DEFAULT_TESTDEF_FORMAT = "Lava-Test Test Definition 1.0"
+DEFAULT_ENVIRONMET_VALUE = "lava_test_shell"
+
+# All these parameters will not be stored on the local config file.
+NAME_PARAMETER = Parameter("name")
+NAME_PARAMETER.store = False
+
+DESCRIPTION_PARAMETER = Parameter("description", depends=NAME_PARAMETER)
+DESCRIPTION_PARAMETER.store = False
+
+TESTDEF_STEPS_KEY = "steps"
+
+TESTDEF_TEMPLATE = {
+ "metadata": {
+ "name": NAME_PARAMETER,
+ "format": DEFAULT_TESTDEF_FORMAT,
+ "version": DEFAULT_TESTDEF_VERSION,
+ "description": DESCRIPTION_PARAMETER,
+ "environment": [DEFAULT_ENVIRONMET_VALUE],
+ },
+ "run": {
+ TESTDEF_STEPS_KEY: ["./mytest.sh"]
+ },
+ "parse": {
+ "pattern": r'^\s*(?P<test_case_id>\w+)=(?P<result>\w+)\s*$'
+ }
+}
diff --git a/lava/testdef/tests/__init__.py b/lava/testdef/tests/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/lava/testdef/tests/__init__.py
diff --git a/lava/testdef/tests/test_commands.py b/lava/testdef/tests/test_commands.py
new file mode 100644
index 0000000..17911ea
--- /dev/null
+++ b/lava/testdef/tests/test_commands.py
@@ -0,0 +1,159 @@
+# Copyright (C) 2013 Linaro Limited
+#
+# Author: Milo Casagrande <milo.casagrande@linaro.org>
+#
+# This file is part of lava-tool.
+#
+# lava-tool is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License version 3
+# as published by the Free Software Foundation
+#
+# lava-tool is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with lava-tool. If not, see <http://www.gnu.org/licenses/>.
+
+"""
+Tests for lava.testdef.commands.
+"""
+
+import os
+import tempfile
+import yaml
+
+from mock import (
+ MagicMock,
+ patch,
+)
+
+from lava.config import InteractiveCache
+from lava.helper.tests.helper_test import HelperTest
+from lava.testdef.commands import (
+ new,
+)
+from lava.tool.errors import CommandError
+
+
+class NewCommandTest(HelperTest):
+
+ """Class for the lava.testdef new command tests."""
+
+ def setUp(self):
+ super(NewCommandTest, self).setUp()
+ self.file_name = "fake_testdef.yaml"
+ self.file_path = os.path.join(tempfile.gettempdir(), self.file_name)
+ self.args.FILE = self.file_path
+
+ self.temp_yaml = tempfile.NamedTemporaryFile(suffix=".yaml",
+ delete=False)
+
+ self.config_file = tempfile.NamedTemporaryFile(delete=False)
+ self.config = InteractiveCache()
+ self.config.save = MagicMock()
+ self.config.config_file = self.config_file.name
+ # Patch class raw_input, start it, and stop it on tearDown.
+ self.patcher1 = patch("lava.parameter.raw_input", create=True)
+ self.mocked_raw_input = self.patcher1.start()
+
+ def tearDown(self):
+ super(NewCommandTest, self).tearDown()
+ if os.path.isfile(self.file_path):
+ os.unlink(self.file_path)
+ os.unlink(self.config_file.name)
+ os.unlink(self.temp_yaml.name)
+ self.patcher1.stop()
+
+ def test_register_arguments(self):
+ # Make sure that the parser add_argument is called and we have the
+ # correct argument.
+ new_command = new(self.parser, self.args)
+ new_command.register_arguments(self.parser)
+
+ # Make sure we do not forget about this test.
+ self.assertEqual(2, len(self.parser.method_calls))
+
+ _, args, _ = self.parser.method_calls[0]
+ self.assertIn("--non-interactive", args)
+
+ _, args, _ = self.parser.method_calls[1]
+ self.assertIn("FILE", args)
+
+ def test_invoke_0(self):
+ # Test that passing a file on the command line, it is created on the
+ # file system.
+ self.mocked_raw_input.return_value = "\n"
+ new_command = new(self.parser, self.args)
+ new_command.config = self.config
+ new_command.invoke()
+ self.assertTrue(os.path.exists(self.file_path))
+
+ def test_invoke_1(self):
+ # Test that when passing an already existing file, an exception is
+ # thrown.
+ self.args.FILE = self.temp_yaml.name
+ new_command = new(self.parser, self.args)
+ new_command.config = self.config
+ self.assertRaises(CommandError, new_command.invoke)
+
+ def test_invoke_2(self):
+ # Tests that when adding a new test definition and writing it to file
+ # a correct YAML structure is created.
+ self.mocked_raw_input.return_value = "\n"
+ new_command = new(self.parser, self.args)
+ new_command.config = self.config
+ new_command.invoke()
+ expected = {'run': {'steps': ["./mytest.sh"]},
+ 'metadata': {
+ 'environment': ['lava_test_shell'],
+ 'format': 'Lava-Test Test Definition 1.0',
+ 'version': '1.0',
+ 'description': '',
+ 'name': ''},
+ 'parse': {
+ 'pattern':
+ '^\\s*(?P<test_case_id>\\w+)=(?P<result>\\w+)\\s*$'
+ },
+ }
+ obtained = None
+ with open(self.file_path, 'r') as read_file:
+ obtained = yaml.load(read_file)
+ self.assertEqual(expected, obtained)
+
+ def test_invoke_3(self):
+ # Tests that when adding a new test definition and writing it to a file
+ # in a directory withour permissions, exception is raised.
+ self.args.FILE = "/test_file.yaml"
+ self.mocked_raw_input.return_value = "\n"
+ new_command = new(self.parser, self.args)
+ new_command.config = self.config
+ self.assertRaises(CommandError, new_command.invoke)
+ self.assertFalse(os.path.exists(self.args.FILE))
+
+ def test_invoke_4(self):
+ # Tests that when passing values for the "steps" ListParameter, we get
+ # back the correct data structure.
+ self.mocked_raw_input.side_effect = ["foo", "\n", "\n", "\n", "\n",
+ "\n"]
+ new_command = new(self.parser, self.args)
+ new_command.config = self.config
+ new_command.invoke()
+ expected = {'run': {'steps': ["./mytest.sh"]},
+ 'metadata': {
+ 'environment': ['lava_test_shell'],
+ 'format': 'Lava-Test Test Definition 1.0',
+ 'version': '1.0',
+ 'description': '',
+ 'name': 'foo'
+ },
+ 'parse': {
+ 'pattern':
+ '^\\s*(?P<test_case_id>\\w+)=(?P<result>\\w+)\\s*$'
+ },
+ }
+ obtained = None
+ with open(self.file_path, 'r') as read_file:
+ obtained = yaml.load(read_file)
+ self.assertEqual(expected, obtained)
diff --git a/lava/tests/__init__.py b/lava/tests/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/lava/tests/__init__.py
diff --git a/lava/tests/test_commands.py b/lava/tests/test_commands.py
new file mode 100644
index 0000000..3033eca
--- /dev/null
+++ b/lava/tests/test_commands.py
@@ -0,0 +1,128 @@
+# Copyright (C) 2013 Linaro Limited
+#
+# Author: Milo Casagrande <milo.casagrande@linaro.org>
+#
+# This file is part of lava-tool.
+#
+# lava-tool is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License version 3
+# as published by the Free Software Foundation
+#
+# lava-tool is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with lava-tool. If not, see <http://www.gnu.org/licenses/>.
+
+"""
+Tests for lava.commands.
+"""
+
+import os
+import tempfile
+
+from mock import (
+ MagicMock,
+ patch
+)
+
+from lava.commands import (
+ init,
+ submit,
+)
+from lava.config import Config
+from lava.helper.tests.helper_test import HelperTest
+from lava.tool.errors import CommandError
+
+
+class InitCommandTests(HelperTest):
+
+ def setUp(self):
+ super(InitCommandTests, self).setUp()
+ self.config_file = self.tmp("init_command_tests")
+ self.config = Config()
+ self.config.config_file = self.config_file
+
+ def tearDown(self):
+ super(InitCommandTests, self).tearDown()
+ if os.path.isfile(self.config_file):
+ os.unlink(self.config_file)
+
+ def test_register_arguments(self):
+ self.args.DIR = os.path.join(tempfile.gettempdir(), "a_fake_dir")
+ init_command = init(self.parser, self.args)
+ init_command.register_arguments(self.parser)
+
+ # Make sure we do not forget about this test.
+ self.assertEqual(2, len(self.parser.method_calls))
+
+ _, args, _ = self.parser.method_calls[0]
+ self.assertIn("--non-interactive", args)
+
+ _, args, _ = self.parser.method_calls[1]
+ self.assertIn("DIR", args)
+
+ @patch("lava.commands.edit_file", create=True)
+ def test_command_invoke_0(self, mocked_edit_file):
+ # Invoke the init command passing a path to a file. Should raise an
+ # exception.
+ self.args.DIR = self.temp_file.name
+ init_command = init(self.parser, self.args)
+ self.assertRaises(CommandError, init_command.invoke)
+
+ def test_command_invoke_2(self):
+ # Invoke the init command passing a path where the user cannot write.
+ try:
+ self.args.DIR = "/root/a_temp_dir"
+ init_command = init(self.parser, self.args)
+ self.assertRaises(CommandError, init_command.invoke)
+ finally:
+ if os.path.exists(self.args.DIR):
+ os.removedirs(self.args.DIR)
+
+ def test_update_data(self):
+ # Make sure the template is updated accordingly with the provided data.
+ self.args.DIR = self.temp_file.name
+
+ init_command = init(self.parser, self.args)
+ init_command.config.get = MagicMock()
+ init_command.config.save = MagicMock()
+ init_command.config.get.side_effect = ["a_job.json"]
+
+ expected = {
+ "jobfile": "a_job.json",
+ }
+
+ obtained = init_command._update_data()
+ self.assertEqual(expected, obtained)
+
+
+class SubmitCommandTests(HelperTest):
+
+ def setUp(self):
+ super(SubmitCommandTests, self).setUp()
+ self.config_file = self.tmp("submit_command_tests")
+ self.config = Config()
+ self.config.config_file = self.config_file
+ self.config.save = MagicMock()
+
+ def tearDown(self):
+ super(SubmitCommandTests, self).tearDown()
+ if os.path.isfile(self.config_file):
+ os.unlink(self.config_file)
+
+ def test_register_arguments(self):
+ self.args.JOB = os.path.join(tempfile.gettempdir(), "a_fake_file")
+ submit_command = submit(self.parser, self.args)
+ submit_command.register_arguments(self.parser)
+
+ # Make sure we do not forget about this test.
+ self.assertEqual(2, len(self.parser.method_calls))
+
+ _, args, _ = self.parser.method_calls[0]
+ self.assertIn("--non-interactive", args)
+
+ _, args, _ = self.parser.method_calls[1]
+ self.assertIn("JOB", args)
diff --git a/lava/tests/test_config.py b/lava/tests/test_config.py
new file mode 100644
index 0000000..737f374
--- /dev/null
+++ b/lava/tests/test_config.py
@@ -0,0 +1,320 @@
+# Copyright (C) 2013 Linaro Limited
+#
+# Author: Milo Casagrande <milo.casagrande@linaro.org>
+#
+# This file is part of lava-tool.
+#
+# lava-tool is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License version 3
+# as published by the Free Software Foundation
+#
+# lava-tool is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with lava-tool. If not, see <http://www.gnu.org/licenses/>.
+
+"""
+lava.config unit tests.
+"""
+
+import os
+import shutil
+import sys
+import tempfile
+
+from StringIO import StringIO
+from mock import (
+ MagicMock,
+ call,
+ patch,
+)
+
+from lava.config import (
+ Config,
+ InteractiveCache,
+ InteractiveConfig,
+)
+from lava.helper.tests.helper_test import HelperTest
+from lava.parameter import (
+ Parameter,
+ ListParameter,
+)
+from lava.tool.errors import CommandError
+
+
+class ConfigTestCase(HelperTest):
+ """General test case class for the different Config classes."""
+ def setUp(self):
+ super(ConfigTestCase, self).setUp()
+ self.param1 = Parameter("foo")
+ self.param2 = Parameter("bar", depends=self.param1)
+
+
+class TestConfigSave(ConfigTestCase):
+
+ """Used to test the save() method of config class.
+
+ Done here since in the other tests we want to mock the atexit save call
+ in order not to write the file, or accidentaly overwrite the real
+ user file.
+ """
+
+ def setUp(self):
+ super(TestConfigSave, self).setUp()
+ self.config = Config()
+ self.config.config_file = self.temp_file.name
+
+ def test_config_save(self):
+ self.config.put_parameter(self.param1, "foo")
+ self.config.save()
+
+ expected = "[DEFAULT]\nfoo = foo\n\n"
+ obtained = ""
+ with open(self.temp_file.name) as tmp_file:
+ obtained = tmp_file.read()
+ self.assertEqual(expected, obtained)
+
+ def test_save_list_param(self):
+ # Tests that when saved to file, the ListParameter parameter is stored
+ # correctly.
+ param_values = ["foo", "more than one words", "bar"]
+ list_param = ListParameter("list")
+ list_param.set(param_values)
+
+ self.config.put_parameter(list_param, param_values)
+ self.config.save()
+
+ expected = "[DEFAULT]\nlist = " + ",".join(param_values) + "\n\n"
+ obtained = ""
+ with open(self.temp_file.name, "r") as read_file:
+ obtained = read_file.read()
+ self.assertEqual(expected, obtained)
+
+
+class ConfigTest(ConfigTestCase):
+
+ def setUp(self):
+ super(ConfigTest, self).setUp()
+
+ self.config_dir = os.path.join(tempfile.gettempdir(), "config")
+ self.xdg_resource = os.path.join(self.config_dir, "linaro")
+ self.lavatool_resource = os.path.join(self.xdg_resource, "lava-tool")
+
+ os.makedirs(self.lavatool_resource)
+
+ self.config = Config()
+ self.config._ensure_xdg_dirs = MagicMock(
+ return_value=self.lavatool_resource)
+ self.config.save = MagicMock()
+
+ def tearDown(self):
+ super(ConfigTest, self).tearDown()
+ if os.path.isdir(self.config_dir):
+ shutil.rmtree(self.config_dir)
+
+ def test_ensure_xdg_dirs(self):
+ # Test that xdg can create the correct cache path, we remove it
+ # at the end since we patch the default value.
+ obtained = self.config._ensure_xdg_dirs()
+ self.assertEquals(self.lavatool_resource, obtained)
+
+ def test_config_file(self):
+ expected = os.path.join(self.lavatool_resource, "lava-tool.ini")
+ obtained = self.config.config_file
+ self.assertEquals(expected, obtained)
+
+ def test_config_put_in_cache_0(self):
+ self.config._put_in_cache("key", "value", "section")
+ self.assertEqual(self.config._cache["section"]["key"], "value")
+
+ def test_config_get_from_cache_0(self):
+ self.config._put_in_cache("key", "value", "section")
+ obtained = self.config._get_from_cache(Parameter("key"), "section")
+ self.assertEqual("value", obtained)
+
+ def test_config_get_from_cache_1(self):
+ self.config._put_in_cache("key", "value", "DEFAULT")
+ obtained = self.config._get_from_cache(Parameter("key"), "DEFAULT")
+ self.assertEqual("value", obtained)
+
+ def test_config_put_0(self):
+ # Puts a value in the DEFAULT section.
+ self.config._put_in_cache = MagicMock()
+ self.config.put("foo", "foo")
+ expected = "foo"
+ obtained = self.config._config_backend.get("DEFAULT", "foo")
+ self.assertEqual(expected, obtained)
+
+ def test_config_put_1(self):
+ # Puts a value in a new section.
+ self.config._put_in_cache = MagicMock()
+ self.config.put("foo", "foo", "bar")
+ expected = "foo"
+ obtained = self.config._config_backend.get("bar", "foo")
+ self.assertEqual(expected, obtained)
+
+ def test_config_put_parameter_0(self):
+ self.config._calculate_config_section = MagicMock(return_value="")
+ self.assertRaises(CommandError, self.config.put_parameter, self.param1)
+
+ @patch("lava.config.Config.put")
+ def test_config_put_parameter_1(self, mocked_config_put):
+ self.config._calculate_config_section = MagicMock(
+ return_value="DEFAULT")
+
+ self.param1.value = "bar"
+ self.config.put_parameter(self.param1)
+
+ self.assertEqual(mocked_config_put.mock_calls,
+ [call("foo", "bar", "DEFAULT")])
+
+ def test_config_get_0(self):
+ # Tests that with a non existing parameter, it returns None.
+ param = Parameter("baz")
+ self.config._get_from_cache = MagicMock(return_value=None)
+ self.config._calculate_config_section = MagicMock(
+ return_value="DEFAULT")
+
+ expected = None
+ obtained = self.config.get(param)
+ self.assertEqual(expected, obtained)
+
+ def test_config_get_1(self):
+ self.config.put_parameter(self.param1, "foo")
+ self.config._get_from_cache = MagicMock(return_value=None)
+ self.config._calculate_config_section = MagicMock(
+ return_value="DEFAULT")
+
+ expected = "foo"
+ obtained = self.config.get(self.param1)
+ self.assertEqual(expected, obtained)
+
+ def test_calculate_config_section_0(self):
+ expected = "DEFAULT"
+ obtained = self.config._calculate_config_section(self.param1)
+ self.assertEqual(expected, obtained)
+
+ def test_calculate_config_section_1(self):
+ self.config.put_parameter(self.param1, "foo")
+ expected = "foo=foo"
+ obtained = self.config._calculate_config_section(self.param2)
+ self.assertEqual(expected, obtained)
+
+ def test_config_get_from_backend_public(self):
+ # Need to to this, since we want a clean Config instance, with
+ # a config_file with some content.
+ with open(self.config.config_file, "w") as write_config:
+ write_config.write("[DEFAULT]\nfoo=bar\n")
+ param = Parameter("foo")
+ obtained = self.config.get_from_backend(param)
+ self.assertEquals("bar", obtained)
+
+
+class InteractiveConfigTest(ConfigTestCase):
+
+ def setUp(self):
+ super(InteractiveConfigTest, self).setUp()
+ self.config = InteractiveConfig()
+ self.config.save = MagicMock()
+ self.config.config_file = self.temp_file.name
+
+ @patch("lava.config.Config.get", new=MagicMock(return_value=None))
+ def test_non_interactive_config_0(self):
+ # Try to get a value that does not exists, users just press enter when
+ # asked for a value. Value will be empty.
+ self.config.force_interactive = False
+ sys.stdin = StringIO("\n")
+ value = self.config.get(Parameter("foo"))
+ self.assertEqual("", value)
+
+ @patch("lava.config.Config.get", new=MagicMock(return_value="value"))
+ def test_non_interactive_config_1(self):
+ # Parent class config returns value, but we are not interactive.
+ self.config.force_interactive = False
+ value = self.config.get(Parameter("foo"))
+ self.assertEqual("value", value)
+
+ @patch("lava.config.Config.get", new=MagicMock(return_value=None))
+ def test_non_interactive_config_2(self):
+ self.config.force_interactive = False
+ expected = "bar"
+ sys.stdin = StringIO(expected)
+ value = self.config.get(Parameter("foo"))
+ self.assertEqual(expected, value)
+
+ @patch("lava.config.Config.get", new=MagicMock(return_value="value"))
+ def test_interactive_config_0(self):
+ # We force to be interactive, meaning that even if a value is found,
+ # it will be asked anyway.
+ self.config.force_interactive = True
+ expected = "a_new_value"
+ sys.stdin = StringIO(expected)
+ value = self.config.get(Parameter("foo"))
+ self.assertEqual(expected, value)
+
+ @patch("lava.config.Config.get", new=MagicMock(return_value="value"))
+ def test_interactive_config_1(self):
+ # Force to be interactive, but when asked for the new value press
+ # Enter. The old value should be returned.
+ self.config.force_interactive = True
+ sys.stdin = StringIO("\n")
+ value = self.config.get(Parameter("foo"))
+ self.assertEqual("value", value)
+
+ def test_calculate_config_section_0(self):
+ self.config.force_interactive = True
+ obtained = self.config._calculate_config_section(self.param1)
+ expected = "DEFAULT"
+ self.assertEqual(expected, obtained)
+
+ def test_calculate_config_section_1(self):
+ self.param1.set("foo")
+ self.param2.depends.asked = True
+ self.config.force_interactive = True
+ obtained = self.config._calculate_config_section(self.param2)
+ expected = "foo=foo"
+ self.assertEqual(expected, obtained)
+
+ def test_calculate_config_section_2(self):
+ self.config.force_interactive = True
+ self.config.config_backend.get = MagicMock(return_value=None)
+ sys.stdin = StringIO("baz")
+ expected = "foo=baz"
+ obtained = self.config._calculate_config_section(self.param2)
+ self.assertEqual(expected, obtained)
+
+ def test_calculate_config_section_3(self):
+ # Tests that when a parameter has its value in the cache and also on
+ # file, we honor the cached version.
+ self.param1.set("bar")
+ self.param2.depends.asked = True
+ self.config.force_interactive = True
+ expected = "foo=bar"
+ obtained = self.config._calculate_config_section(self.param2)
+ self.assertEqual(expected, obtained)
+
+ @patch("lava.config.Config.get", new=MagicMock(return_value=None))
+ @patch("lava.parameter.sys.exit")
+ @patch("lava.parameter.raw_input", create=True)
+ def test_interactive_config_exit(self, mocked_raw, mocked_sys_exit):
+ self.config._calculate_config_section = MagicMock(
+ return_value="DEFAULT")
+
+ mocked_raw.side_effect = KeyboardInterrupt()
+
+ self.config.force_interactive = True
+ self.config.get(self.param1)
+ self.assertTrue(mocked_sys_exit.called)
+
+ @patch("lava.parameter.raw_input", create=True)
+ def test_interactive_config_with_list_parameter(self, mocked_raw_input):
+ # Tests that we get a list back in the Config class when using
+ # ListParameter and that it contains the expected values.
+ expected = ["foo", "bar"]
+ mocked_raw_input.side_effect = expected + ["\n"]
+ obtained = self.config.get(ListParameter("list"))
+ self.assertIsInstance(obtained, list)
+ self.assertEqual(expected, obtained)
diff --git a/lava/tests/test_parameter.py b/lava/tests/test_parameter.py
new file mode 100644
index 0000000..2c1f76d
--- /dev/null
+++ b/lava/tests/test_parameter.py
@@ -0,0 +1,206 @@
+# Copyright (C) 2013 Linaro Limited
+#
+# Author: Milo Casagrande <milo.casagrande@linaro.org>
+#
+# This file is part of lava-tool.
+#
+# lava-tool is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License version 3
+# as published by the Free Software Foundation
+#
+# lava-tool is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with lava-tool. If not, see <http://www.gnu.org/licenses/>.
+
+"""
+lava.parameter unit tests.
+"""
+
+from mock import patch
+
+from lava.helper.tests.helper_test import HelperTest
+from lava.parameter import (
+ ListParameter,
+ Parameter,
+ SingleChoiceParameter,
+)
+from lava_tool.utils import to_list
+
+
+class GeneralParameterTest(HelperTest):
+ """General class with setUp and tearDown methods for Parameter tests."""
+ def setUp(self):
+ super(GeneralParameterTest, self).setUp()
+ # Patch class raw_input, start it, and stop it on tearDown.
+ self.patcher1 = patch("lava.parameter.raw_input", create=True)
+ self.mocked_raw_input = self.patcher1.start()
+
+ def tearDown(self):
+ super(GeneralParameterTest, self).tearDown()
+ self.patcher1.stop()
+
+
+class ParameterTest(GeneralParameterTest):
+ """Tests for the Parameter class."""
+
+ def setUp(self):
+ super(ParameterTest, self).setUp()
+ self.parameter1 = Parameter("foo", value="baz")
+
+ def test_prompt_0(self):
+ # Tests that when we have a value in the parameters and the user press
+ # Enter, we get the old value back.
+ self.mocked_raw_input.return_value = "\n"
+ obtained = self.parameter1.prompt()
+ self.assertEqual(self.parameter1.value, obtained)
+
+ def test_prompt_1(self,):
+ # Tests that with a value stored in the parameter, if and EOFError is
+ # raised when getting user input, we get back the old value.
+ self.mocked_raw_input.side_effect = EOFError()
+ obtained = self.parameter1.prompt()
+ self.assertEqual(self.parameter1.value, obtained)
+
+ def test_to_list_0(self):
+ value = "a_value"
+ expected = [value]
+ obtained = to_list(value)
+ self.assertIsInstance(obtained, list)
+ self.assertEquals(expected, obtained)
+
+ def test_to_list_1(self):
+ expected = ["a_value", "b_value"]
+ obtained = to_list(expected)
+ self.assertIsInstance(obtained, list)
+ self.assertEquals(expected, obtained)
+
+
+class ListParameterTest(GeneralParameterTest):
+
+ """Tests for the specialized ListParameter class."""
+
+ def setUp(self):
+ super(ListParameterTest, self).setUp()
+ self.list_parameter = ListParameter("list")
+
+ def test_prompt_0(self):
+ # Test that when pressing Enter, the prompt stops and the list is
+ # returned.
+ expected = []
+ self.mocked_raw_input.return_value = "\n"
+ obtained = self.list_parameter.prompt()
+ self.assertEqual(expected, obtained)
+
+ def test_prompt_1(self):
+ # Tests that when passing 3 values, a list with those values
+ # is returned
+ expected = ["foo", "bar", "foobar"]
+ self.mocked_raw_input.side_effect = expected + ["\n"]
+ obtained = self.list_parameter.prompt()
+ self.assertEqual(expected, obtained)
+
+ def test_serialize_0(self):
+ # Tests the serialize method of ListParameter passing a list.
+ expected = "foo,bar,baz,1"
+ to_serialize = ["foo", "bar", "baz", "", 1]
+
+ obtained = self.list_parameter.serialize(to_serialize)
+ self.assertEqual(expected, obtained)
+
+ def test_serialize_1(self):
+ # Tests the serialize method of ListParameter passing an int.
+ expected = "1"
+ to_serialize = 1
+
+ obtained = self.list_parameter.serialize(to_serialize)
+ self.assertEqual(expected, obtained)
+
+ def test_deserialize_0(self):
+ # Tests the deserialize method of ListParameter with a string
+ # of values.
+ expected = ["foo", "bar", "baz"]
+ to_deserialize = "foo,bar,,baz,"
+ obtained = self.list_parameter.deserialize(to_deserialize)
+ self.assertEqual(expected, obtained)
+
+ def test_deserialize_1(self):
+ # Tests the deserialization method of ListParameter passing a list.
+ expected = ["foo", 1, "", "bar"]
+ obtained = self.list_parameter.deserialize(expected)
+ self.assertEqual(expected, obtained)
+
+ def test_set_value_0(self):
+ # Pass a string to a ListParameter, expect a list.
+ set_value = "foo"
+ expected = [set_value]
+ self.list_parameter.set(set_value)
+ self.assertEquals(expected, self.list_parameter.value)
+
+ def test_set_value_1(self):
+ # Pass a list to a ListParameter, expect the same list.
+ expected = ["foo", "bar"]
+ self.list_parameter.set(expected)
+ self.assertEquals(expected, self.list_parameter.value)
+
+ def test_add_value_0(self):
+ # Add a value to a ListParameter, expect a list back.
+ add_value = "foo"
+ expected = [add_value]
+ self.list_parameter.add(add_value)
+ self.assertEquals(expected, self.list_parameter.value)
+
+ def test_add_value_1(self):
+ # Add a list value to a ListParameter with already a value set, expect
+ # a list with both values.
+ # The ListParameter is initialized with a string.
+ add_value = ["foo"]
+ list_param = ListParameter("list", value="bar")
+ expected = ["bar", "foo"]
+ list_param.add(add_value)
+ self.assertEquals(expected, list_param.value)
+
+ def test_add_value_2(self):
+ # Add a list value to a ListParameter with already a value set, expect
+ # a list with both values.
+ # The ListParameter is initialized with a list.
+ add_value = ["foo"]
+ list_param = ListParameter("list", value=["bar", "baz"])
+ expected = ["bar", "baz", "foo"]
+ list_param.add(add_value)
+ self.assertEquals(expected, list_param.value)
+
+
+class TestsSingleChoiceParameter(GeneralParameterTest):
+
+ def setUp(self):
+ super(TestsSingleChoiceParameter, self).setUp()
+ self.choices = ["foo", "bar", "baz", "bam"]
+ self.param_id = "single_choice"
+ self.single_choice_param = SingleChoiceParameter(self.param_id,
+ self.choices)
+
+ def test_with_old_value(self):
+ # There is an old value for a single choice parameter, the user
+ # is prompted to select from the list of values, but she presses
+ # enter. The old value is returned.
+ old_value = "bat"
+ self.mocked_raw_input.side_effect = ["\n"]
+ obtained = self.single_choice_param.prompt("", old_value=old_value)
+ self.assertEquals(old_value, obtained)
+
+ def test_without_old_value(self):
+ # There is no old value, user just select the first choice.
+ self.mocked_raw_input.side_effect = ["1"]
+ obtained = self.single_choice_param.prompt("")
+ self.assertEquals("foo", obtained)
+
+ def test_with_wrong_user_input(self):
+ # No old value, user inserts at least two wrong choices, and the select
+ # the third one.
+ self.mocked_raw_input.side_effect = ["1000", "0", "3"]
+ obtained = self.single_choice_param.prompt("")
+ self.assertEquals("baz", obtained)
diff --git a/lava/tool/__init__.py b/lava/tool/__init__.py
index 8f5d8fa..97f7f0b 100644
--- a/lava/tool/__init__.py
+++ b/lava/tool/__init__.py
@@ -24,4 +24,4 @@ lava.tool
Generic code for command line utilities for LAVA
"""
-__version__ = (0, 7, 1, "final", 0)
+__version__ = (0, 8, 0, "final", 0)
diff --git a/lava/tool/commands/__init__.py b/lava/tool/commands/__init__.py
index d4928d4..e69de29 100644
--- a/lava/tool/commands/__init__.py
+++ b/lava/tool/commands/__init__.py
@@ -1,83 +0,0 @@
-# Copyright (C) 2010 Linaro Limited
-#
-# Author: Zygmunt Krynicki <zygmunt.krynicki@linaro.org>
-#
-# This file is part of lava-tool.
-#
-# lava-tool is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Lesser General Public License version 3
-# as published by the Free Software Foundation
-#
-# lava-tool is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU Lesser General Public License
-# along with lava-tool. If not, see <http://www.gnu.org/licenses/>.
-
-"""
-Package with command line commands
-"""
-
-import argparse
-import re
-
-
-class ExperimentalNoticeAction(argparse.Action):
- """
- Argparse action that implements the --experimental-notice
- """
-
- message = """
- Some lc-tool sub-commands are marked as EXPERIMENTAL. Those commands are
- not guaranteed to work identically, or have identical interface between
- subsequent lc-tool releases.
-
- We do that to make it possible to provide good user interface and
- server-side API when working on new features. Once a feature is stabilized
- the UI will be frozen and all subsequent changes will retain backwards
- compatibility.
- """
- message = message.lstrip()
- message = re.sub(re.compile("[ \t]+", re.M), " ", message)
- message = re.sub(re.compile("^ ", re.M), "", message)
-
- def __init__(self,
- option_strings, dest, default=None, required=False,
- help=None):
- super(ExperimentalNoticeAction, self).__init__(
- option_strings=option_strings, dest=dest, default=default, nargs=0,
- help=help)
-
- def __call__(self, parser, namespace, values, option_string=None):
- parser.exit(message=self.message)
-
-
-class ExperimentalCommandMixIn(object):
- """
- Experimental command.
-
- Prints a warning message on each call to invoke()
- """
-
- def invoke(self):
- self.print_experimental_notice()
- return super(ExperimentalCommandMixIn, self).invoke()
-
- @classmethod
- def register_arguments(cls, parser):
- retval = super(ExperimentalCommandMixIn,
- cls).register_arguments(parser)
- parser.register("action", "experimental_notice",
- ExperimentalNoticeAction)
- group = parser.add_argument_group("experimental commands")
- group.add_argument("--experimental-notice",
- action="experimental_notice",
- default=argparse.SUPPRESS,
- help="Explain the nature of experimental commands")
- return retval
-
- def print_experimental_notice(self):
- print ("EXPERIMENTAL - SUBJECT TO CHANGE"
- " (See --experimental-notice for more info)")
diff --git a/lava/tool/dispatcher.py b/lava/tool/dispatcher.py
index 1256912..ff94be8 100644
--- a/lava/tool/dispatcher.py
+++ b/lava/tool/dispatcher.py
@@ -21,6 +21,7 @@ Module with LavaDispatcher - the command dispatcher
"""
import argparse
+import argcomplete
import logging
import pkg_resources
import sys
@@ -29,6 +30,7 @@ from lava.tool.errors import CommandError
class Dispatcher(object):
+
"""
Class implementing command line interface for launch control
"""
@@ -39,7 +41,7 @@ class Dispatcher(object):
def __init__(self, parser=None, name=None):
self.parser = parser or self.construct_parser()
self.subparsers = self.parser.add_subparsers(
- title="Sub-command to invoke")
+ title="Sub-command to invoke")
self.name = name
def __repr__(self):
@@ -72,7 +74,9 @@ class Dispatcher(object):
try:
command_cls = entrypoint.load()
except (ImportError, pkg_resources.DistributionNotFound) as exc:
- logging.exception("Unable to load command: %s", entrypoint.name)
+ logging.exception(
+ "Unable to load command: %s",
+ entrypoint.name)
else:
self.add_command_cls(command_cls)
@@ -89,7 +93,7 @@ class Dispatcher(object):
command_cls.get_name(),
help=command_cls.get_help(),
epilog=command_cls.get_epilog())
- from lava.tool.command import CommandGroup
+ from lava.tool.command import CommandGroup
if issubclass(command_cls, CommandGroup):
# Handle CommandGroup somewhat different. Instead of calling
# register_arguments we call register_subcommands
@@ -121,6 +125,8 @@ class Dispatcher(object):
If arguments are left out they are looked up in sys.argv automatically
"""
+ # Before anything, hook into the bash completion
+ argcomplete.autocomplete(self.parser)
# First parse whatever input arguments we've got
args = self.parser.parse_args(raw_args)
# Adjust logging level after seeing arguments
diff --git a/lava/tool/errors.py b/lava/tool/errors.py
index 6f8f0e4..8d520f7 100644
--- a/lava/tool/errors.py
+++ b/lava/tool/errors.py
@@ -23,7 +23,9 @@ lava.tool.errors
Error classes for LAVA Tool.
"""
+
class CommandError(Exception):
+
"""
Raise this from a Command's invoke() method to display an error nicely.
diff --git a/lava/tool/main.py b/lava/tool/main.py
index d151961..e3b43ce 100644
--- a/lava/tool/main.py
+++ b/lava/tool/main.py
@@ -78,7 +78,7 @@ class LavaDispatcher(Dispatcher):
default=[],
help="Enable debugging of the specified logger, can be specified multiple times")
# Return the improved parser
- return parser
+ return parser
def setup_logging(self):
"""
@@ -96,6 +96,7 @@ class LavaDispatcher(Dispatcher):
logging.Formatter("%(levelname)s: %(message)s"))
err_handler.addFilter(OnlyProblemsFilter())
logging.getLogger().addHandler(err_handler)
+
# Enable the debug handler
class DebugFilter(logging.Filter):
def filter(self, record):
@@ -113,6 +114,7 @@ class LavaDispatcher(Dispatcher):
# Enable verbose message handler
if args.verbose:
logging.getLogger().setLevel(logging.INFO)
+
class OnlyInfoFilter(logging.Filterer):
def filter(self, record):
if record.levelno == logging.INFO:
@@ -124,7 +126,7 @@ class LavaDispatcher(Dispatcher):
logging.Formatter("%(message)s"))
msg_handler.addFilter(OnlyInfoFilter())
logging.getLogger().addHandler(msg_handler)
- # Enable debugging
+ # Enable debugging
if args.debug:
logging.getLogger().setLevel(logging.DEBUG)
# Enable trace loggers