aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAntonio Terceiro <antonio.terceiro@linaro.org>2013-11-18 18:47:24 -0300
committerAntonio Terceiro <antonio.terceiro@linaro.org>2013-11-18 18:47:24 -0300
commit99ea8ab8541c3144c5e62cec8cdd327dc13c4388 (patch)
tree67c033d678e6ea7b0223d9608e5bf7dcd4752f20
parent49c36d653c19c018e473d7a1a27bdf5cb607c4be (diff)
Imported Upstream version 0.8upstream/0.8
-rw-r--r--PKG-INFO2
-rw-r--r--README33
-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
-rw-r--r--lava_dashboard_tool/commands.py239
-rw-r--r--lava_dashboard_tool/tests/__init__.py52
-rw-r--r--lava_scheduler_tool/commands.py37
-rw-r--r--lava_tool.egg-info/PKG-INFO2
-rw-r--r--lava_tool.egg-info/SOURCES.txt42
-rw-r--r--lava_tool.egg-info/entry_points.txt158
-rw-r--r--lava_tool.egg-info/requires.txt5
-rw-r--r--lava_tool.egg-info/top_level.txt2
-rw-r--r--lava_tool/authtoken.py28
-rw-r--r--lava_tool/commands/__init__.py21
-rw-r--r--lava_tool/commands/auth.py14
-rw-r--r--lava_tool/tests/__init__.py34
-rw-r--r--lava_tool/tests/test_authtoken.py78
-rw-r--r--lava_tool/tests/test_commands.py15
-rw-r--r--lava_tool/tests/test_utils.py282
-rw-r--r--lava_tool/utils.py329
-rwxr-xr-xsetup.py76
62 files changed, 5125 insertions, 482 deletions
diff --git a/PKG-INFO b/PKG-INFO
index cf53501..d055310 100644
--- a/PKG-INFO
+++ b/PKG-INFO
@@ -1,6 +1,6 @@
Metadata-Version: 1.1
Name: lava-tool
-Version: 0.7.1
+Version: 0.8
Summary: Command line utility for Linaro validation services
Home-page: https://launchpad.net/lava-tool
Author: Zygmunt Krynicki
diff --git a/README b/README
index 00755cf..8b47082 100644
--- a/README
+++ b/README
@@ -14,6 +14,39 @@ Installation
See INSTALL
+Usage
+=====
+
+Dealing with jobs
+
+ $ lava job new file.json # creates file.json from a template
+ $ lava job submit file.json # submits file.json to a remote LAVA server
+ $ lava job run file.json # runs file.json on a local LAVA device
+
+Dealing with LAVA Test Shell Test Definitions
+
+ $ lava testdef new file.yml # creates file.yml from a template
+ $ lava testdef submit file.yml # submits file.yml to a remote LAVA server
+ $ lava testdef run file.yml # runs file.yml on a local LAVA device
+
+Dealing with LAVA Test Shell Scripts
+
+ $ lava script submit SCRIPT # submits SCRIPT to a remote LAVA server
+ $ lava script run SCRIPT # runs SCRIPT on a local LAVA device
+
+Bash completion
+===============
+
+Once lava-tool is installed, you can turn bash completion on for the `lava` and
+`lava-tool` programs with the following commands (which you can also paste in
+your ~/.bashrc):
+
+ eval "$(register-python-argcomplete lava)"
+ eval "$(register-python-argcomplete lava-tool)"
+
+Then if you type for example "lava-tool su<TAB>", it will complete that "su"
+with "submit-job" for you.
+
Reporting Bugs
==============
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
diff --git a/lava_dashboard_tool/commands.py b/lava_dashboard_tool/commands.py
index 1159681..1d6a25e 100644
--- a/lava_dashboard_tool/commands.py
+++ b/lava_dashboard_tool/commands.py
@@ -37,11 +37,11 @@ import simplejson
from json_schema_validator.extensions import datetime_extension
from lava_tool.authtoken import AuthenticatingServerProxy, KeyringAuthBackend
-from lava.tool.commands import ExperimentalCommandMixIn
from lava.tool.command import Command, CommandGroup
class dashboard(CommandGroup):
+
"""
Commands for interacting with LAVA Dashboard
"""
@@ -50,16 +50,19 @@ class dashboard(CommandGroup):
class InsufficientServerVersion(Exception):
+
"""
Exception raised when server version that a command interacts with is too
old to support required features.
"""
+
def __init__(self, server_version, required_version):
self.server_version = server_version
self.required_version = required_version
class DataSetRenderer(object):
+
"""
Support class for rendering a table out of list of dictionaries.
@@ -77,8 +80,10 @@ class DataSetRenderer(object):
Each dictionary must have the same keys. In particular the first row
is used to determine columns.
"""
+
def __init__(self, column_map=None, row_formatter=None, empty=None,
- order=None, caption=None, separator=" ", header_separator=None):
+ order=None, caption=None, separator=" ",
+ header_separator=None):
if column_map is None:
column_map = {}
if row_formatter is None:
@@ -179,11 +184,11 @@ class DataSetRenderer(object):
if column in self.row_formatter:
row[column] = self.row_formatter[column](row[column])
maxlen = dict(
- [(column, max(
- len(self.column_map.get(column, column)),
- max([
- len(str(row[column])) for row in dataset_out])))
- for column in columns])
+ [(column, max(
+ len(self.column_map.get(column, column)),
+ max([
+ len(str(row[column])) for row in dataset_out])))
+ for column in columns])
return dataset_out, columns, maxlen
def _render_header(self, dataset, columns, maxlen):
@@ -244,7 +249,7 @@ class DataSetRenderer(object):
# Now print the coulum names
print self.separator.join([
"{0:^{1}}".format(self.column_map.get(column, column),
- maxlen[column]) for column in columns])
+ maxlen[column]) for column in columns])
# Finally print the header separator
if self.header_separator:
print "-" * total_len
@@ -265,7 +270,7 @@ class DataSetRenderer(object):
>>> maxlen = {'a': 13, 'bee': 3}
Now a plain table. Note! To really understand this test
- you should check out the length of the strings below. There
+ you should check out the whitespace in the strings below. There
are two more spaces after 'b' in the second row
>>> DataSetRenderer()._render_rows(dataset, columns, maxlen)
shorter
@@ -306,6 +311,7 @@ class DataSetRenderer(object):
class XMLRPCCommand(Command):
+
"""
Abstract base class for commands that interact with dashboard server
over XML-RPC.
@@ -344,7 +350,7 @@ class XMLRPCCommand(Command):
'0.4.0b2'
"""
try:
- major, minor, micro, releaselevel, serial = version.split(".")
+ major, minor, micro, releaselevel, serial = version.split(".")
except ValueError:
raise ValueError(
("version %r does not follow pattern "
@@ -377,7 +383,7 @@ class XMLRPCCommand(Command):
def __init__(self, parser, args):
super(XMLRPCCommand, self).__init__(parser, args)
- xml_rpc_url = self._construct_xml_rpc_url(self.args.dashboard_url)
+ xml_rpc_url = self._construct_xml_rpc_url(self.args.dashboard_url)
self.server = AuthenticatingServerProxy(
xml_rpc_url,
verbose=args.verbose_xml_rpc,
@@ -393,19 +399,20 @@ class XMLRPCCommand(Command):
@classmethod
def register_arguments(cls, parser):
- dashboard_group = parser.add_argument_group("dashboard specific arguments")
+ dashboard_group = parser.add_argument_group(
+ "dashboard specific arguments")
default_dashboard_url = os.getenv("DASHBOARD_URL")
if default_dashboard_url:
dashboard_group.add_argument("--dashboard-url",
- metavar="URL", help="URL of your validation dashboard (currently %(default)s)",
- default=default_dashboard_url)
+ metavar="URL", help="URL of your validation dashboard (currently %(default)s)",
+ default=default_dashboard_url)
else:
dashboard_group.add_argument("--dashboard-url", required=True,
- metavar="URL", help="URL of your validation dashboard")
+ metavar="URL", help="URL of your validation dashboard")
debug_group = parser.add_argument_group("debugging arguments")
debug_group.add_argument("--verbose-xml-rpc",
- action="store_true", default=False,
- help="Show XML-RPC data")
+ action="store_true", default=False,
+ help="Show XML-RPC data")
return dashboard_group
@contextlib.contextmanager
@@ -414,7 +421,7 @@ class XMLRPCCommand(Command):
yield
except socket.error as ex:
print >> sys.stderr, "Unable to connect to server at %s" % (
- self.args.dashboard_url,)
+ self.args.dashboard_url,)
# It seems that some errors are reported as -errno
# while others as +errno.
ex.errno = abs(ex.errno)
@@ -429,12 +436,13 @@ class XMLRPCCommand(Command):
print >> sys.stderr, "Socket %d: %s" % (ex.errno, ex.strerror)
except xmlrpclib.ProtocolError as ex:
print >> sys.stderr, "Unable to exchange XML-RPC message with dashboard server"
- print >> sys.stderr, "HTTP error code: %d/%s" % (ex.errcode, ex.errmsg)
+ print >> sys.stderr, "HTTP error code: %d/%s" % (
+ ex.errcode, ex.errmsg)
except xmlrpclib.Fault as ex:
self.handle_xmlrpc_fault(ex.faultCode, ex.faultString)
except InsufficientServerVersion as ex:
print >> sys.stderr, ("This command requires at least server version "
- "%s, actual server version is %s" %
+ "%s, actual server version is %s" %
(ex.required_version, ex.server_version))
def invoke(self):
@@ -447,13 +455,15 @@ class XMLRPCCommand(Command):
print >> sys.stderr, "Dashboard server has experienced internal error"
print >> sys.stderr, faultString
else:
- print >> sys.stderr, "XML-RPC error %d: %s" % (faultCode, faultString)
+ print >> sys.stderr, "XML-RPC error %d: %s" % (
+ faultCode, faultString)
def invoke_remote(self):
raise NotImplementedError()
class server_version(XMLRPCCommand):
+
"""
Display dashboard server version
"""
@@ -463,6 +473,7 @@ class server_version(XMLRPCCommand):
class put(XMLRPCCommand):
+
"""
Upload a bundle on the server
"""
@@ -471,11 +482,11 @@ class put(XMLRPCCommand):
def register_arguments(cls, parser):
super(put, cls).register_arguments(parser)
parser.add_argument("LOCAL",
- type=argparse.FileType("rb"),
- help="pathname on the local file system")
+ type=argparse.FileType("rb"),
+ help="pathname on the local file system")
parser.add_argument("REMOTE",
- default="/anonymous/", nargs='?',
- help="pathname on the server")
+ default="/anonymous/", nargs='?',
+ help="pathname on the server")
def invoke_remote(self):
content = self.args.LOCAL.read()
@@ -487,7 +498,7 @@ class put(XMLRPCCommand):
def handle_xmlrpc_fault(self, faultCode, faultString):
if faultCode == 404:
print >> sys.stderr, "Bundle stream %s does not exist" % (
- self.args.REMOTE)
+ self.args.REMOTE)
elif faultCode == 409:
print >> sys.stderr, "You have already uploaded this bundle to the dashboard"
else:
@@ -495,6 +506,7 @@ class put(XMLRPCCommand):
class get(XMLRPCCommand):
+
"""
Download a bundle from the server
"""
@@ -503,15 +515,15 @@ class get(XMLRPCCommand):
def register_arguments(cls, parser):
super(get, cls).register_arguments(parser)
parser.add_argument("SHA1",
- type=str,
- help="SHA1 of the bundle to download")
+ type=str,
+ help="SHA1 of the bundle to download")
parser.add_argument("--overwrite",
- action="store_true",
- help="Overwrite files on the local disk")
+ action="store_true",
+ help="Overwrite files on the local disk")
parser.add_argument("--output", "-o",
- type=argparse.FileType("wb"),
- default=None,
- help="Alternate name of the output file")
+ type=argparse.FileType("wb"),
+ default=None,
+ help="Alternate name of the output file")
def invoke_remote(self):
response = self.server.get(self.args.SHA1)
@@ -519,7 +531,7 @@ class get(XMLRPCCommand):
filename = self.args.SHA1
if os.path.exists(filename) and not self.args.overwrite:
print >> sys.stderr, "File {filename!r} already exists".format(
- filename=filename)
+ filename=filename)
print >> sys.stderr, "You may pass --overwrite to write over it"
return -1
stream = open(filename, "wb")
@@ -528,17 +540,18 @@ class get(XMLRPCCommand):
filename = self.args.output.name
stream.write(response['content'])
print "Downloaded bundle {0} to file {1!r}".format(
- self.args.SHA1, filename)
+ self.args.SHA1, filename)
def handle_xmlrpc_fault(self, faultCode, faultString):
if faultCode == 404:
print >> sys.stderr, "Bundle {sha1} does not exist".format(
- sha1=self.args.SHA1)
+ sha1=self.args.SHA1)
else:
super(get, self).handle_xmlrpc_fault(faultCode, faultString)
class deserialize(XMLRPCCommand):
+
"""
Deserialize a bundle on the server
"""
@@ -547,8 +560,8 @@ class deserialize(XMLRPCCommand):
def register_arguments(cls, parser):
super(deserialize, cls).register_arguments(parser)
parser.add_argument("SHA1",
- type=str,
- help="SHA1 of the bundle to deserialize")
+ type=str,
+ help="SHA1 of the bundle to deserialize")
def invoke_remote(self):
response = self.server.deserialize(self.args.SHA1)
@@ -558,13 +571,17 @@ class deserialize(XMLRPCCommand):
def handle_xmlrpc_fault(self, faultCode, faultString):
if faultCode == 404:
print >> sys.stderr, "Bundle {sha1} does not exist".format(
- sha1=self.args.SHA1)
+ sha1=self.args.SHA1)
elif faultCode == 409:
print >> sys.stderr, "Unable to deserialize bundle {sha1}".format(
sha1=self.args.SHA1)
print >> sys.stderr, faultString
else:
- super(deserialize, self).handle_xmlrpc_fault(faultCode, faultString)
+ super(
+ deserialize,
+ self).handle_xmlrpc_fault(
+ faultCode,
+ faultString)
def _get_pretty_renderer(**kwargs):
@@ -576,6 +593,7 @@ def _get_pretty_renderer(**kwargs):
class streams(XMLRPCCommand):
+
"""
Show streams you have access to
"""
@@ -596,33 +614,34 @@ class streams(XMLRPCCommand):
class bundles(XMLRPCCommand):
+
"""
Show bundles in the specified stream
"""
renderer = _get_pretty_renderer(
- column_map={
- 'uploaded_by': 'Uploader',
- 'uploaded_on': 'Upload date',
- 'content_filename': 'File name',
- 'content_sha1': 'SHA1',
- 'is_deserialized': "Deserialized?"},
- row_formatter={
- 'is_deserialized': lambda x: "yes" if x else "no",
- 'uploaded_by': lambda x: x or "(anonymous)",
- 'uploaded_on': lambda x: x},
- order=('content_sha1', 'content_filename', 'uploaded_by',
- 'uploaded_on', 'is_deserialized'),
- empty="There are no bundles in this stream",
- caption="Bundles",
- separator=" | ")
+ column_map={
+ 'uploaded_by': 'Uploader',
+ 'uploaded_on': 'Upload date',
+ 'content_filename': 'File name',
+ 'content_sha1': 'SHA1',
+ 'is_deserialized': "Deserialized?"},
+ row_formatter={
+ 'is_deserialized': lambda x: "yes" if x else "no",
+ 'uploaded_by': lambda x: x or "(anonymous)",
+ 'uploaded_on': lambda x: x},
+ order=('content_sha1', 'content_filename', 'uploaded_by',
+ 'uploaded_on', 'is_deserialized'),
+ empty="There are no bundles in this stream",
+ caption="Bundles",
+ separator=" | ")
@classmethod
def register_arguments(cls, parser):
super(bundles, cls).register_arguments(parser)
parser.add_argument("PATHNAME",
- default="/anonymous/", nargs='?',
- help="pathname on the server (defaults to %(default)s)")
+ default="/anonymous/", nargs='?',
+ help="pathname on the server (defaults to %(default)s)")
def invoke_remote(self):
self.renderer.render(self.server.bundles(self.args.PATHNAME))
@@ -630,12 +649,13 @@ class bundles(XMLRPCCommand):
def handle_xmlrpc_fault(self, faultCode, faultString):
if faultCode == 404:
print >> sys.stderr, "Bundle stream %s does not exist" % (
- self.args.PATHNAME)
+ self.args.PATHNAME)
else:
super(bundles, self).handle_xmlrpc_fault(faultCode, faultString)
class make_stream(XMLRPCCommand):
+
"""
Create a bundle stream on the server
"""
@@ -660,9 +680,9 @@ class make_stream(XMLRPCCommand):
class backup(XMLRPCCommand):
+
"""
Backup data uploaded to a dashboard instance.
-
Not all data is preserved. The following data is lost: identity of the user
that uploaded each bundle, time of uploading and deserialization on the
server, name of the bundle stream that contained the data
@@ -679,7 +699,9 @@ class backup(XMLRPCCommand):
os.mkdir(self.args.BACKUP_DIR)
for bundle_stream in self.server.streams():
print "Processing stream %s" % bundle_stream["pathname"]
- bundle_stream_dir = os.path.join(self.args.BACKUP_DIR, urllib.quote_plus(bundle_stream["pathname"]))
+ bundle_stream_dir = os.path.join(
+ self.args.BACKUP_DIR,
+ urllib.quote_plus(bundle_stream["pathname"]))
if not os.path.exists(bundle_stream_dir):
os.mkdir(bundle_stream_dir)
with open(os.path.join(bundle_stream_dir, "metadata.json"), "wt") as stream:
@@ -692,14 +714,18 @@ class backup(XMLRPCCommand):
for bundle in self.server.bundles(bundle_stream["pathname"]):
print " * Backing up bundle %s" % bundle["content_sha1"]
data = self.server.get(bundle["content_sha1"])
- bundle_pathname = os.path.join(bundle_stream_dir, bundle["content_sha1"])
- # Note: we write bundles as binary data to preserve anything the user might have dumped on us
+ bundle_pathname = os.path.join(
+ bundle_stream_dir,
+ bundle["content_sha1"])
+ # Note: we write bundles as binary data to preserve anything
+ # the user might have dumped on us
with open(bundle_pathname + ".json", "wb") as stream:
stream.write(data["content"])
with open(bundle_pathname + ".metadata.json", "wt") as stream:
simplejson.dump({
"uploaded_by": bundle["uploaded_by"],
- "uploaded_on": datetime_extension.to_json(bundle["uploaded_on"]),
+ "uploaded_on":
+ datetime_extension.to_json(bundle["uploaded_on"]),
"content_filename": bundle["content_filename"],
"content_sha1": bundle["content_sha1"],
"content_size": bundle["content_size"],
@@ -707,6 +733,7 @@ class backup(XMLRPCCommand):
class restore(XMLRPCCommand):
+
"""
Restore a dashboard instance from backup
"""
@@ -720,7 +747,9 @@ class restore(XMLRPCCommand):
def invoke_remote(self):
self._check_server_version(self.server, "0.3")
for stream_pathname_quoted in os.listdir(self.args.BACKUP_DIR):
- filesystem_stream_pathname = os.path.join(self.args.BACKUP_DIR, stream_pathname_quoted)
+ filesystem_stream_pathname = os.path.join(
+ self.args.BACKUP_DIR,
+ stream_pathname_quoted)
if not os.path.isdir(filesystem_stream_pathname):
continue
stream_pathname = urllib.unquote(stream_pathname_quoted)
@@ -731,12 +760,18 @@ class restore(XMLRPCCommand):
stream_metadata = {}
print "Processing stream %s" % stream_pathname
try:
- self.server.make_stream(stream_pathname, stream_metadata.get("name", "Restored from backup"))
+ self.server.make_stream(
+ stream_pathname,
+ stream_metadata.get(
+ "name",
+ "Restored from backup"))
except xmlrpclib.Fault as ex:
if ex.faultCode != 409:
raise
for content_sha1 in [item[:-len(".json")] for item in os.listdir(filesystem_stream_pathname) if item.endswith(".json") and not item.endswith(".metadata.json") and item != "metadata.json"]:
- filesystem_content_filename = os.path.join(filesystem_stream_pathname, content_sha1 + ".json")
+ filesystem_content_filename = os.path.join(
+ filesystem_stream_pathname,
+ content_sha1 + ".json")
if not os.path.isfile(filesystem_content_filename):
continue
with open(os.path.join(filesystem_stream_pathname, content_sha1) + ".metadata.json", "rt") as stream:
@@ -745,16 +780,18 @@ class restore(XMLRPCCommand):
content = stream.read()
print " * Restoring bundle %s" % content_sha1
try:
- self.server.put(content, bundle_metadata["content_filename"], stream_pathname)
+ self.server.put(
+ content,
+ bundle_metadata["content_filename"],
+ stream_pathname)
except xmlrpclib.Fault as ex:
if ex.faultCode != 409:
raise
-
-class pull(ExperimentalCommandMixIn, XMLRPCCommand):
+
+class pull(XMLRPCCommand):
"""
Copy bundles and bundle streams from one dashboard to another.
-
This command checks for two environment varialbes:
The value of DASHBOARD_URL is used as a replacement for --dashbard-url.
The value of REMOTE_DASHBOARD_URL as a replacement for FROM.
@@ -785,7 +822,10 @@ class pull(ExperimentalCommandMixIn, XMLRPCCommand):
group.add_argument(
"FROM",
help="URL of the remote validation dashboard)")
- group.add_argument("STREAM", nargs="*", help="Streams to pull from (all by default)")
+ group.add_argument(
+ "STREAM",
+ nargs="*",
+ help="Streams to pull from (all by default)")
@staticmethod
def _filesizeformat(num_bytes):
@@ -810,7 +850,7 @@ class pull(ExperimentalCommandMixIn, XMLRPCCommand):
def invoke_remote(self):
self._check_server_version(self.server, "0.3")
-
+
print "Checking local and remote streams"
remote = self.remote_server.streams()
if self.args.STREAM:
@@ -819,28 +859,37 @@ class pull(ExperimentalCommandMixIn, XMLRPCCommand):
remote_set = frozenset((stream["pathname"] for stream in remote))
unavailable_set = requested_set - remote_set
if unavailable_set:
- print >> sys.stderr, "Remote stream not found: %s" % ", ".join(unavailable_set)
+ print >> sys.stderr, "Remote stream not found: %s" % ", ".join(
+ unavailable_set)
return -1
# Limit to requested streams if necessary
- remote = [stream for stream in remote if stream["pathname"] in requested_set]
+ remote = [
+ stream for stream in remote if stream[
+ "pathname"] in requested_set]
local = self.server.streams()
- missing_pathnames = set([stream["pathname"] for stream in remote]) - set([stream["pathname"] for stream in local])
+ missing_pathnames = set([stream["pathname"]
+ for stream in remote]) - set([stream["pathname"] for stream in local])
for stream in remote:
if stream["pathname"] in missing_pathnames:
self.server.make_stream(stream["pathname"], stream["name"])
local_bundles = []
else:
- local_bundles = [bundle for bundle in self.server.bundles(stream["pathname"])]
- remote_bundles = [bundle for bundle in self.remote_server.bundles(stream["pathname"])]
- missing_bundles = set((bundle["content_sha1"] for bundle in remote_bundles))
- missing_bundles -= set((bundle["content_sha1"] for bundle in local_bundles))
+ local_bundles = [
+ bundle for bundle in self.server.bundles(stream["pathname"])]
+ remote_bundles = [
+ bundle for bundle in self.remote_server.bundles(stream["pathname"])]
+ missing_bundles = set(
+ (bundle["content_sha1"] for bundle in remote_bundles))
+ missing_bundles -= set(
+ (bundle["content_sha1"] for bundle in local_bundles))
try:
missing_bytes = sum(
(bundle["content_size"]
for bundle in remote_bundles
if bundle["content_sha1"] in missing_bundles))
except KeyError as ex:
- # Older servers did not return content_size so this part is optional
+ # Older servers did not return content_size so this part is
+ # optional
missing_bytes = None
if missing_bytes:
print "Stream %s needs update (%s)" % (stream["pathname"], self._filesizeformat(missing_bytes))
@@ -855,17 +904,20 @@ class pull(ExperimentalCommandMixIn, XMLRPCCommand):
print "got %s, storing" % (self._filesizeformat(len(data["content"]))),
sys.stdout.flush()
try:
- self.server.put(data["content"], data["content_filename"], stream["pathname"])
+ self.server.put(
+ data["content"],
+ data["content_filename"],
+ stream["pathname"])
except xmlrpclib.Fault as ex:
if ex.faultCode == 409: # duplicate
print "already present (in another stream)"
else:
raise
- else:
+ else:
print "done"
-class data_views(ExperimentalCommandMixIn, XMLRPCCommand):
+class data_views(XMLRPCCommand):
"""
Show data views defined on the server
"""
@@ -885,7 +937,7 @@ class data_views(ExperimentalCommandMixIn, XMLRPCCommand):
print "Tip: to invoke a data view try `lc-tool query-data-view`"
-class query_data_view(ExperimentalCommandMixIn, XMLRPCCommand):
+class query_data_view(XMLRPCCommand):
"""
Invoke a specified data view
"""
@@ -893,7 +945,8 @@ class query_data_view(ExperimentalCommandMixIn, XMLRPCCommand):
def register_arguments(cls, parser):
super(query_data_view, cls).register_arguments(parser)
parser.add_argument("QUERY", metavar="QUERY", nargs="...",
- help="Data view name and any optional and required arguments")
+ help="Data view name and any optional \
+ and required arguments")
def _probe_data_views(self):
"""
@@ -914,7 +967,7 @@ class query_data_view(ExperimentalCommandMixIn, XMLRPCCommand):
del parser._actions[-1]
subparsers = parser.add_subparsers(
title="Data views available on the server")
- for data_view in self.data_views:
+ for data_view in self.data_views:
data_view_parser = subparsers.add_parser(
data_view["name"],
help=data_view["summary"],
@@ -924,14 +977,16 @@ class query_data_view(ExperimentalCommandMixIn, XMLRPCCommand):
for argument in data_view["arguments"]:
if argument["default"] is None:
group.add_argument(
- "--{name}".format(name=argument["name"].replace("_", "-")),
+ "--{name}".format(
+ name=argument["name"].replace("_", "-")),
dest=argument["name"],
help=argument["help"],
type=str,
required=True)
else:
group.add_argument(
- "--{name}".format(name=argument["name"].replace("_", "-")),
+ "--{name}".format(
+ name=argument["name"].replace("_", "-")),
dest=argument["name"],
help=argument["help"],
type=str,
@@ -953,9 +1008,11 @@ class query_data_view(ExperimentalCommandMixIn, XMLRPCCommand):
for argument in self.args.data_view["arguments"]:
arg_name = argument["name"]
if arg_name in self.args:
- data_view_args[arg_name] = getattr(self.args, arg_name)
+ data_view_args[arg_name] = getattr(self.args, arg_name)
# Invoke the data view
- response = self.server.query_data_view(self.args.data_view["name"], data_view_args)
+ response = self.server.query_data_view(
+ self.args.data_view["name"],
+ data_view_args)
# Create a pretty-printer
renderer = _get_pretty_renderer(
caption=self.args.data_view["summary"],
@@ -971,9 +1028,11 @@ class query_data_view(ExperimentalCommandMixIn, XMLRPCCommand):
class version(Command):
+
"""
Show dashboard client version
"""
+
def invoke(self):
import versiontools
from lava_dashboard_tool import __version__
diff --git a/lava_dashboard_tool/tests/__init__.py b/lava_dashboard_tool/tests/__init__.py
index 725aa2a..e69de29 100644
--- a/lava_dashboard_tool/tests/__init__.py
+++ b/lava_dashboard_tool/tests/__init__.py
@@ -1,52 +0,0 @@
-# Copyright (C) 2010,2011 Linaro Limited
-#
-# Author: Zygmunt Krynicki <zygmunt.krynicki@linaro.org>
-#
-# This file is part of lava-dashboard-tool.
-#
-# lava-dashboard-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-dashboard-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-dashboard-tool. If not, see <http://www.gnu.org/licenses/>.
-
-"""
-Package with unit tests for lava_dashboard_tool
-"""
-
-import doctest
-import unittest
-
-
-def app_modules():
- return [
- 'lava_dashboard_tool.commands',
- ]
-
-
-def test_modules():
- return [
- 'lava_dashboard_tool.tests.test_commands',
- ]
-
-
-def test_suite():
- """
- Build an unittest.TestSuite() object with all the tests in _modules.
- Each module is harvested for both regular unittests and doctests
- """
- modules = app_modules() + test_modules()
- suite = unittest.TestSuite()
- loader = unittest.TestLoader()
- for name in modules:
- unit_suite = loader.loadTestsFromName(name)
- suite.addTests(unit_suite)
- doc_suite = doctest.DocTestSuite(name)
- suite.addTests(doc_suite)
- return suite
diff --git a/lava_scheduler_tool/commands.py b/lava_scheduler_tool/commands.py
index e0ea7bf..5844538 100644
--- a/lava_scheduler_tool/commands.py
+++ b/lava_scheduler_tool/commands.py
@@ -24,7 +24,6 @@ import xmlrpclib
from lava_tool.authtoken import AuthenticatingServerProxy, KeyringAuthBackend
from lava.tool.command import Command, CommandGroup
from lava.tool.errors import CommandError
-from lava.tool.commands import ExperimentalCommandMixIn
class scheduler(CommandGroup):
@@ -35,7 +34,7 @@ class scheduler(CommandGroup):
namespace = "lava.scheduler.commands"
-class submit_job(ExperimentalCommandMixIn, Command):
+class submit_job(Command):
"""
Submit a job to lava-scheduler
"""
@@ -47,7 +46,6 @@ class submit_job(ExperimentalCommandMixIn, Command):
parser.add_argument("JSON_FILE")
def invoke(self):
- self.print_experimental_notice()
server = AuthenticatingServerProxy(
self.args.SERVER, auth_backend=KeyringAuthBackend())
with open(self.args.JSON_FILE, 'rb') as stream:
@@ -60,15 +58,14 @@ class submit_job(ExperimentalCommandMixIn, Command):
print "submitted as job id:", job_id
-class resubmit_job(ExperimentalCommandMixIn, Command):
+class resubmit_job(Command):
@classmethod
def register_arguments(self, parser):
parser.add_argument("SERVER")
- parser.add_argument("JOB_ID", type=int)
+ parser.add_argument("JOB_ID")
def invoke(self):
- self.print_experimental_notice()
server = AuthenticatingServerProxy(
self.args.SERVER, auth_backend=KeyringAuthBackend())
try:
@@ -79,15 +76,14 @@ class resubmit_job(ExperimentalCommandMixIn, Command):
print "resubmitted as job id:", job_id
-class cancel_job(ExperimentalCommandMixIn, Command):
+class cancel_job(Command):
@classmethod
def register_arguments(self, parser):
parser.add_argument("SERVER")
- parser.add_argument("JOB_ID", type=int)
+ parser.add_argument("JOB_ID")
def invoke(self):
- self.print_experimental_notice()
server = AuthenticatingServerProxy(
self.args.SERVER, auth_backend=KeyringAuthBackend())
server.scheduler.cancel_job(self.args.JOB_ID)
@@ -103,7 +99,6 @@ class job_output(Command):
super(job_output, cls).register_arguments(parser)
parser.add_argument("SERVER")
parser.add_argument("JOB_ID",
- type=int,
help="Job ID to download output file")
parser.add_argument("--overwrite",
action="store_true",
@@ -132,3 +127,25 @@ class job_output(Command):
print "Downloaded job output of {0} to file {1!r}".format(
self.args.JOB_ID, filename)
+
+
+class job_status(Command):
+ """
+ Get job status and bundle sha1, if it existed, from the scheduler.
+ """
+
+ @classmethod
+ def register_arguments(cls, parser):
+ super(job_status, cls).register_arguments(parser)
+ parser.add_argument("SERVER")
+ parser.add_argument("JOB_ID",
+ help="Job ID to check the status")
+
+ def invoke(self):
+ server = AuthenticatingServerProxy(
+ self.args.SERVER, auth_backend=KeyringAuthBackend())
+ job_status = server.scheduler.job_status(self.args.JOB_ID)
+
+ print "Job ID: %s\nJob Status: %s\nBundle SHA1: %s" % \
+ (str(self.args.JOB_ID), job_status['job_status'],
+ job_status['bundle_sha1'])
diff --git a/lava_tool.egg-info/PKG-INFO b/lava_tool.egg-info/PKG-INFO
index cf53501..d055310 100644
--- a/lava_tool.egg-info/PKG-INFO
+++ b/lava_tool.egg-info/PKG-INFO
@@ -1,6 +1,6 @@
Metadata-Version: 1.1
Name: lava-tool
-Version: 0.7.1
+Version: 0.8
Summary: Command line utility for Linaro validation services
Home-page: https://launchpad.net/lava-tool
Author: Zygmunt Krynicki
diff --git a/lava_tool.egg-info/SOURCES.txt b/lava_tool.egg-info/SOURCES.txt
index ad7a633..31c1297 100644
--- a/lava_tool.egg-info/SOURCES.txt
+++ b/lava_tool.egg-info/SOURCES.txt
@@ -2,6 +2,44 @@ README
setup.cfg
setup.py
lava/__init__.py
+lava/commands.py
+lava/config.py
+lava/parameter.py
+lava/device/__init__.py
+lava/device/commands.py
+lava/device/templates.py
+lava/device/tests/__init__.py
+lava/device/tests/test_commands.py
+lava/device/tests/test_device.py
+lava/helper/__init__.py
+lava/helper/command.py
+lava/helper/dispatcher.py
+lava/helper/template.py
+lava/helper/tests/__init__.py
+lava/helper/tests/helper_test.py
+lava/helper/tests/test_command.py
+lava/helper/tests/test_dispatcher.py
+lava/helper/tests/test_template.py
+lava/job/__init__.py
+lava/job/commands.py
+lava/job/templates.py
+lava/job/tests/__init__.py
+lava/job/tests/test_commands.py
+lava/job/tests/test_job.py
+lava/script/__init__.py
+lava/script/commands.py
+lava/script/tests/__init__.py
+lava/script/tests/test_commands.py
+lava/script/tests/test_script.py
+lava/testdef/__init__.py
+lava/testdef/commands.py
+lava/testdef/templates.py
+lava/testdef/tests/__init__.py
+lava/testdef/tests/test_commands.py
+lava/tests/__init__.py
+lava/tests/test_commands.py
+lava/tests/test_config.py
+lava/tests/test_parameter.py
lava/tool/__init__.py
lava/tool/command.py
lava/tool/dispatcher.py
@@ -20,6 +58,7 @@ lava_tool/__init__.py
lava_tool/authtoken.py
lava_tool/dispatcher.py
lava_tool/interface.py
+lava_tool/utils.py
lava_tool.egg-info/PKG-INFO
lava_tool.egg-info/SOURCES.txt
lava_tool.egg-info/dependency_links.txt
@@ -33,4 +72,5 @@ lava_tool/commands/auth.py
lava_tool/tests/__init__.py
lava_tool/tests/test_auth_commands.py
lava_tool/tests/test_authtoken.py
-lava_tool/tests/test_commands.py \ No newline at end of file
+lava_tool/tests/test_commands.py
+lava_tool/tests/test_utils.py \ No newline at end of file
diff --git a/lava_tool.egg-info/entry_points.txt b/lava_tool.egg-info/entry_points.txt
index ab6d3d7..2266d9e 100644
--- a/lava_tool.egg-info/entry_points.txt
+++ b/lava_tool.egg-info/entry_points.txt
@@ -1,63 +1,97 @@
+[console_scripts]
+lava-tool = lava_tool.dispatcher:main
+lava = lava.tool.main:LavaDispatcher.run
+lava-dashboard-tool=lava_dashboard_tool.main:main
- [console_scripts]
- lava-tool = lava_tool.dispatcher:main
- lava = lava.tool.main:LavaDispatcher.run
- lava-dashboard-tool=lava_dashboard_tool.main:main
- [lava.commands]
- help = lava.tool.commands.help:help
- scheduler = lava_scheduler_tool.commands:scheduler
- dashboard = lava_dashboard_tool.commands:dashboard
- [lava_tool.commands]
- help = lava.tool.commands.help:help
- auth-add = lava_tool.commands.auth:auth_add
- submit-job = lava_scheduler_tool.commands:submit_job
- resubmit-job = lava_scheduler_tool.commands:resubmit_job
- cancel-job = lava_scheduler_tool.commands:cancel_job
- job-output = lava_scheduler_tool.commands:job_output
- backup=lava_dashboard_tool.commands:backup
- bundles=lava_dashboard_tool.commands:bundles
- data_views=lava_dashboard_tool.commands:data_views
- deserialize=lava_dashboard_tool.commands:deserialize
- get=lava_dashboard_tool.commands:get
- make_stream=lava_dashboard_tool.commands:make_stream
- pull=lava_dashboard_tool.commands:pull
- put=lava_dashboard_tool.commands:put
- query_data_view=lava_dashboard_tool.commands:query_data_view
- restore=lava_dashboard_tool.commands:restore
- server_version=lava_dashboard_tool.commands:server_version
- streams=lava_dashboard_tool.commands:streams
- version=lava_dashboard_tool.commands:version
- [lava.scheduler.commands]
- submit-job = lava_scheduler_tool.commands:submit_job
- resubmit-job = lava_scheduler_tool.commands:resubmit_job
- cancel-job = lava_scheduler_tool.commands:cancel_job
- job-output = lava_scheduler_tool.commands:job_output
- [lava.dashboard.commands]
- backup=lava_dashboard_tool.commands:backup
- bundles=lava_dashboard_tool.commands:bundles
- data_views=lava_dashboard_tool.commands:data_views
- deserialize=lava_dashboard_tool.commands:deserialize
- get=lava_dashboard_tool.commands:get
- make_stream=lava_dashboard_tool.commands:make_stream
- pull=lava_dashboard_tool.commands:pull
- put=lava_dashboard_tool.commands:put
- query_data_view=lava_dashboard_tool.commands:query_data_view
- restore=lava_dashboard_tool.commands:restore
- server_version=lava_dashboard_tool.commands:server_version
- streams=lava_dashboard_tool.commands:streams
- version=lava_dashboard_tool.commands:version
- [lava_dashboard_tool.commands]
- backup=lava_dashboard_tool.commands:backup
- bundles=lava_dashboard_tool.commands:bundles
- data_views=lava_dashboard_tool.commands:data_views
- deserialize=lava_dashboard_tool.commands:deserialize
- get=lava_dashboard_tool.commands:get
- make_stream=lava_dashboard_tool.commands:make_stream
- pull=lava_dashboard_tool.commands:pull
- put=lava_dashboard_tool.commands:put
- query_data_view=lava_dashboard_tool.commands:query_data_view
- restore=lava_dashboard_tool.commands:restore
- server_version=lava_dashboard_tool.commands:server_version
- streams=lava_dashboard_tool.commands:streams
- version=lava_dashboard_tool.commands:version
- \ No newline at end of file
+[lava.commands]
+help = lava.tool.commands.help:help
+scheduler = lava_scheduler_tool.commands:scheduler
+dashboard = lava_dashboard_tool.commands:dashboard
+job = lava.job.commands:job
+device = lava.device.commands:device
+testdef = lava.testdef.commands:testdef
+init = lava.commands:init
+submit = lava.commands:submit
+run = lava.commands:run
+status = lava.job.commands:status
+update = lava.commands:update
+script = lava.script.commands:script
+
+[lava_tool.commands]
+help = lava.tool.commands.help:help
+auth-add = lava_tool.commands.auth:auth_add
+submit-job = lava_scheduler_tool.commands:submit_job
+resubmit-job = lava_scheduler_tool.commands:resubmit_job
+cancel-job = lava_scheduler_tool.commands:cancel_job
+job-output = lava_scheduler_tool.commands:job_output
+job-status = lava_scheduler_tool.commands:job_status
+backup=lava_dashboard_tool.commands:backup
+bundles=lava_dashboard_tool.commands:bundles
+data_views=lava_dashboard_tool.commands:data_views
+deserialize=lava_dashboard_tool.commands:deserialize
+get=lava_dashboard_tool.commands:get
+make_stream=lava_dashboard_tool.commands:make_stream
+pull=lava_dashboard_tool.commands:pull
+put=lava_dashboard_tool.commands:put
+query_data_view=lava_dashboard_tool.commands:query_data_view
+restore=lava_dashboard_tool.commands:restore
+server_version=lava_dashboard_tool.commands:server_version
+streams=lava_dashboard_tool.commands:streams
+version=lava_dashboard_tool.commands:version
+
+[lava.scheduler.commands]
+submit-job = lava_scheduler_tool.commands:submit_job
+resubmit-job = lava_scheduler_tool.commands:resubmit_job
+cancel-job = lava_scheduler_tool.commands:cancel_job
+job-output = lava_scheduler_tool.commands:job_output
+job-status = lava_scheduler_tool.commands:job_status
+
+[lava.dashboard.commands]
+backup=lava_dashboard_tool.commands:backup
+bundles=lava_dashboard_tool.commands:bundles
+data_views=lava_dashboard_tool.commands:data_views
+deserialize=lava_dashboard_tool.commands:deserialize
+get=lava_dashboard_tool.commands:get
+make_stream=lava_dashboard_tool.commands:make_stream
+pull=lava_dashboard_tool.commands:pull
+put=lava_dashboard_tool.commands:put
+query_data_view=lava_dashboard_tool.commands:query_data_view
+restore=lava_dashboard_tool.commands:restore
+server_version=lava_dashboard_tool.commands:server_version
+streams=lava_dashboard_tool.commands:streams
+version=lava_dashboard_tool.commands:version
+
+[lava_dashboard_tool.commands]
+backup=lava_dashboard_tool.commands:backup
+bundles=lava_dashboard_tool.commands:bundles
+data_views=lava_dashboard_tool.commands:data_views
+deserialize=lava_dashboard_tool.commands:deserialize
+get=lava_dashboard_tool.commands:get
+make_stream=lava_dashboard_tool.commands:make_stream
+pull=lava_dashboard_tool.commands:pull
+put=lava_dashboard_tool.commands:put
+query_data_view=lava_dashboard_tool.commands:query_data_view
+restore=lava_dashboard_tool.commands:restore
+server_version=lava_dashboard_tool.commands:server_version
+streams=lava_dashboard_tool.commands:streams
+version=lava_dashboard_tool.commands:version
+
+[lava.job.commands]
+new = lava.job.commands:new
+submit = lava.job.commands:submit
+status = lava.job.commands:status
+run = lava.job.commands:run
+
+[lava.device.commands]
+add = lava.device.commands:add
+remove = lava.device.commands:remove
+config = lava.device.commands:config
+
+[lava.testdef.commands]
+new = lava.testdef.commands:new
+run = lava.testdef.commands:run
+submit = lava.testdef.commands:submit
+
+[lava.script.commands]
+run = lava.script.commands:run
+submit = lava.script.commands:submit
diff --git a/lava_tool.egg-info/requires.txt b/lava_tool.egg-info/requires.txt
index 499b0fd..721c7ce 100644
--- a/lava_tool.egg-info/requires.txt
+++ b/lava_tool.egg-info/requires.txt
@@ -1,4 +1,7 @@
+PyYAML >= 3.10
argparse >= 1.1
+argcomplete >= 0.3
keyring
json-schema-validator >= 2.0
-versiontools >= 1.3.1 \ No newline at end of file
+versiontools >= 1.3.1
+pyxdg == 0.25 \ No newline at end of file
diff --git a/lava_tool.egg-info/top_level.txt b/lava_tool.egg-info/top_level.txt
index d297271..a2bffe4 100644
--- a/lava_tool.egg-info/top_level.txt
+++ b/lava_tool.egg-info/top_level.txt
@@ -1,4 +1,4 @@
-lava_scheduler_tool
lava_dashboard_tool
lava_tool
+lava_scheduler_tool
lava
diff --git a/lava_tool/authtoken.py b/lava_tool/authtoken.py
index 70837cd..621277b 100644
--- a/lava_tool/authtoken.py
+++ b/lava_tool/authtoken.py
@@ -27,6 +27,16 @@ import keyring.core
from lava_tool.interface import LavaCommandError
+def normalize_xmlrpc_url(uri):
+ if '://' not in uri:
+ uri = 'http://' + uri
+ if not uri.endswith('/'):
+ uri += '/'
+ if not uri.endswith('/RPC2/'):
+ uri += 'RPC2/'
+ return uri
+
+
class AuthBackend(object):
def add_token(self, username, endpoint_url, token):
@@ -72,6 +82,15 @@ class XMLRPCTransport(xmlrpclib.Transport):
def request(self, host, handler, request_body, verbose=0):
self.verbose = verbose
+ request = self.build_http_request(host, handler, request_body)
+ try:
+ response = self._opener.open(request)
+ except urllib2.HTTPError as e:
+ raise xmlrpclib.ProtocolError(
+ host + handler, e.code, e.msg, e.info())
+ return self.parse_response(response)
+
+ def build_http_request(self, host, handler, request_body):
token = None
user = None
auth, host = urllib.splituser(host)
@@ -88,18 +107,15 @@ class XMLRPCTransport(xmlrpclib.Transport):
if token:
auth = base64.b64encode(urllib.unquote(user + ':' + token))
request.add_header("Authorization", "Basic " + auth)
- try:
- response = self._opener.open(request)
- except urllib2.HTTPError as e:
- raise xmlrpclib.ProtocolError(
- host + handler, e.code, e.msg, e.info())
- return self.parse_response(response)
+
+ return request
class AuthenticatingServerProxy(xmlrpclib.ServerProxy):
def __init__(self, uri, transport=None, encoding=None, verbose=0,
allow_none=0, use_datetime=0, auth_backend=None):
+ uri = normalize_xmlrpc_url(uri)
if transport is None:
scheme = urllib.splittype(uri)[0]
transport = XMLRPCTransport(scheme, auth_backend=auth_backend)
diff --git a/lava_tool/commands/__init__.py b/lava_tool/commands/__init__.py
index f8bf829..7a58207 100644
--- a/lava_tool/commands/__init__.py
+++ b/lava_tool/commands/__init__.py
@@ -1,24 +1,3 @@
-# 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
"""
-
-
-from lava.tool.commands import ExperimentalNoticeAction, ExperimentalCommandMixIn
diff --git a/lava_tool/commands/auth.py b/lava_tool/commands/auth.py
index 0e0f343..da4f19a 100644
--- a/lava_tool/commands/auth.py
+++ b/lava_tool/commands/auth.py
@@ -24,21 +24,13 @@ from lava_tool.authtoken import (
AuthenticatingServerProxy,
KeyringAuthBackend,
MemoryAuthBackend,
- )
+ normalize_xmlrpc_url,
+)
from lava_tool.interface import Command, LavaCommandError
-def normalize_xmlrpc_url(uri):
- if '://' not in uri:
- uri = 'http://' + uri
- if not uri.endswith('/'):
- uri += '/'
- if not uri.endswith('/RPC2/'):
- uri += 'RPC2/'
- return uri
-
-
class auth_add(Command):
+
"""
Add an authentication token.
"""
diff --git a/lava_tool/tests/__init__.py b/lava_tool/tests/__init__.py
index cd2d2fd..92e6531 100644
--- a/lava_tool/tests/__init__.py
+++ b/lava_tool/tests/__init__.py
@@ -26,19 +26,34 @@ import unittest
def app_modules():
return [
- 'lava_tool.commands',
- 'lava_tool.commands.misc',
- 'lava_tool.dispatcher',
- 'lava_tool.interface',
- ]
+ 'lava_tool.commands',
+ 'lava_tool.dispatcher',
+ 'lava_tool.interface',
+ 'lava_dashboard_tool.commands',
+ ]
def test_modules():
return [
- 'lava_tool.tests.test_authtoken',
- 'lava_tool.tests.test_auth_commands',
- 'lava_tool.tests.test_commands',
- ]
+ 'lava.device.tests.test_commands',
+ 'lava.device.tests.test_device',
+ 'lava.helper.tests.test_command',
+ 'lava.helper.tests.test_dispatcher',
+ 'lava.helper.tests.test_template',
+ 'lava.job.tests.test_commands',
+ 'lava.job.tests.test_job',
+ 'lava.script.tests.test_commands',
+ 'lava.script.tests.test_script',
+ 'lava.testdef.tests.test_commands',
+ 'lava.tests.test_commands',
+ 'lava.tests.test_config',
+ 'lava.tests.test_parameter',
+ 'lava_dashboard_tool.tests.test_commands',
+ 'lava_tool.tests.test_auth_commands',
+ 'lava_tool.tests.test_authtoken',
+ 'lava_tool.tests.test_commands',
+ 'lava_tool.tests.test_utils',
+ ]
def test_suite():
@@ -49,6 +64,7 @@ def test_suite():
modules = app_modules() + test_modules()
suite = unittest.TestSuite()
loader = unittest.TestLoader()
+
for name in modules:
unit_suite = loader.loadTestsFromName(name)
suite.addTests(unit_suite)
diff --git a/lava_tool/tests/test_authtoken.py b/lava_tool/tests/test_authtoken.py
index 12e0151..da24c8c 100644
--- a/lava_tool/tests/test_authtoken.py
+++ b/lava_tool/tests/test_authtoken.py
@@ -24,81 +24,47 @@ import base64
import StringIO
from unittest import TestCase
import urlparse
-import sys
import xmlrpclib
from mocker import ARGS, KWARGS, Mocker
from lava_tool.authtoken import (
AuthenticatingServerProxy,
+ XMLRPCTransport,
MemoryAuthBackend,
- )
+)
from lava_tool.interface import LavaCommandError
-if sys.version_info[:2] <= (2, 6):
- TWO_SIX = True
-else:
- TWO_SIX = False
class TestAuthenticatingServerProxy(TestCase):
def auth_headers_for_method_call_on(self, url, auth_backend):
parsed = urlparse.urlparse(url)
- expected_host = parsed.hostname
- if parsed.port:
- expected_host += ':' + str(parsed.port)
- server_proxy = AuthenticatingServerProxy(
- url, auth_backend=auth_backend)
+
mocker = Mocker()
- if url.startswith('https'):
- cls_name = 'httplib.HTTPS'
- expected_constructor_args = (expected_host, ARGS)
- else:
- cls_name = 'httplib.HTTP'
- expected_constructor_args = (expected_host, ARGS)
- if not TWO_SIX:
- cls_name += 'Connection'
- mocked_HTTPConnection = mocker.replace(cls_name, passthrough=False)
- mocked_connection = mocked_HTTPConnection(*expected_constructor_args)
- # nospec() is required because of
- # https://bugs.launchpad.net/mocker/+bug/794351
- mocker.nospec()
+ transport = mocker.mock()
+
auth_data = []
- mocked_connection.putrequest(ARGS, KWARGS)
- if TWO_SIX:
- mocked_connection.send(ARGS, KWARGS)
-
- def match_header(header, *values):
- if header.lower() == 'authorization':
- if len(values) != 1:
- self.fail(
- 'more than one value for '
- 'putheader("Authorization", ...)')
- auth_data.append(values[0])
- mocked_connection.putheader(ARGS)
- mocker.call(match_header)
- mocker.count(1, None)
-
- mocked_connection.endheaders(ARGS, KWARGS)
-
- if TWO_SIX:
- mocked_connection.getreply(ARGS, KWARGS)
- mocker.result((200, None, None))
- s = StringIO.StringIO(xmlrpclib.dumps((1,), methodresponse=True))
- mocked_connection.getfile()
- mocker.result(s)
- mocked_connection._conn
- mocker.result(None)
- else:
- mocked_connection.getresponse(ARGS, KWARGS)
- s = StringIO.StringIO(xmlrpclib.dumps((1,), methodresponse=True))
- s.status = 200
- mocker.result(s)
- mocked_connection.close()
- mocker.count(0, 1)
+ def intercept_request(host, handler, request_body, verbose=0):
+ actual_transport = XMLRPCTransport(parsed.scheme, auth_backend)
+ request = actual_transport.build_http_request(
+ host, handler, request_body)
+ if (request.has_header('Authorization')):
+ auth_data.append(request.get_header('Authorization'))
+
+ response_body = xmlrpclib.dumps((1,), methodresponse=True)
+ response = StringIO.StringIO(response_body)
+ response.status = 200
+ response.__len__ = lambda: len(response_body)
+
+ transport.request(ARGS, KWARGS)
+ mocker.call(intercept_request)
+ mocker.result(response)
with mocker:
+ server_proxy = AuthenticatingServerProxy(
+ url, auth_backend=auth_backend, transport=transport)
server_proxy.method()
return auth_data
diff --git a/lava_tool/tests/test_commands.py b/lava_tool/tests/test_commands.py
index 0961b6a..b961f7b 100644
--- a/lava_tool/tests/test_commands.py
+++ b/lava_tool/tests/test_commands.py
@@ -20,16 +20,16 @@
Unit tests for the launch_control.commands package
"""
-from mocker import MockerTestCase
+from mocker import MockerTestCase, ARGS
from lava_tool.interface import (
Command,
LavaCommandError,
- )
+)
from lava_tool.dispatcher import (
LavaDispatcher,
main,
- )
+)
class CommandTestCase(MockerTestCase):
@@ -59,6 +59,7 @@ class CommandTestCase(MockerTestCase):
def test_get_help_uses_docstring(self):
class ASDF(Command):
+
"""
This command was named after the lisp package management system
"""
@@ -80,6 +81,7 @@ class CommandTestCase(MockerTestCase):
def test_get_epilog_returns_data_after_carriage_L(self):
# The dot after 'before' is to make pep8 happy
class help_with_epilog(Command):
+
"""
before
.
@@ -90,6 +92,7 @@ class CommandTestCase(MockerTestCase):
def test_get_help_returns_data_before_carriage_L(self):
# The dot after 'before' is to make pep8 happy
class help_with_epilog(Command):
+
"""
before
.
@@ -101,10 +104,10 @@ class CommandTestCase(MockerTestCase):
class DispatcherTestCase(MockerTestCase):
def test_main(self):
- mock_LavaDispatcher = self.mocker.replace(
- 'lava_tool.dispatcher.LavaDispatcher')
- mock_LavaDispatcher().dispatch()
+ dispatcher = self.mocker.patch(LavaDispatcher)
+ dispatcher.dispatch(ARGS)
self.mocker.replay()
+
self.assertRaises(SystemExit, main)
def test_add_command_cls(self):
diff --git a/lava_tool/tests/test_utils.py b/lava_tool/tests/test_utils.py
new file mode 100644
index 0000000..dd1c937
--- /dev/null
+++ b/lava_tool/tests/test_utils.py
@@ -0,0 +1,282 @@
+# 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_tool.utils tests."""
+
+import os
+import shutil
+import subprocess
+import sys
+import tempfile
+
+from unittest import TestCase
+from mock import (
+ MagicMock,
+ call,
+ patch,
+)
+
+from lava.tool.errors import CommandError
+from lava_tool.utils import (
+ can_edit_file,
+ create_dir,
+ edit_file,
+ execute,
+ has_command,
+ retrieve_file,
+ verify_and_create_url,
+ verify_file_extension,
+)
+
+
+class UtilTests(TestCase):
+
+ def setUp(self):
+ 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.temp_file = tempfile.NamedTemporaryFile(delete=False)
+
+ def tearDown(self):
+ sys.stdin = self.original_stdin
+ sys.stdout = self.original_stdout
+ sys.stderr = self.original_stderr
+ os.unlink(self.temp_file.name)
+
+ @patch("lava_tool.utils.subprocess.check_call")
+ def test_has_command_0(self, mocked_check_call):
+ # Make sure we raise an exception when the subprocess is called.
+ mocked_check_call.side_effect = subprocess.CalledProcessError(0, "")
+ self.assertFalse(has_command(""))
+
+ @patch("lava_tool.utils.subprocess.check_call")
+ def test_has_command_1(self, mocked_check_call):
+ # Check that a "command" exists. The call to subprocess is mocked.
+ mocked_check_call.return_value = 0
+ self.assertTrue(has_command(""))
+
+ def test_verify_file_extension_with_extension(self):
+ extension = ".test"
+ supported = [extension[1:]]
+ try:
+ temp_file = tempfile.NamedTemporaryFile(suffix=extension,
+ delete=False)
+ obtained = verify_file_extension(
+ temp_file.name, extension[1:], supported)
+ self.assertEquals(temp_file.name, obtained)
+ finally:
+ if os.path.isfile(temp_file.name):
+ os.unlink(temp_file.name)
+
+ def test_verify_file_extension_without_extension(self):
+ extension = "json"
+ supported = [extension]
+ expected = "/tmp/a_fake.json"
+ obtained = verify_file_extension("/tmp/a_fake", extension, supported)
+ self.assertEquals(expected, obtained)
+
+ def test_verify_file_extension_with_unsupported_extension(self):
+ extension = "json"
+ supported = [extension]
+ expected = "/tmp/a_fake.json"
+ obtained = verify_file_extension(
+ "/tmp/a_fake.extension", extension, supported)
+ self.assertEquals(expected, obtained)
+
+ @patch("os.listdir")
+ def test_retrieve_job_file_0(self, mocked_os_listdir):
+ # Make sure that exception is raised if we go through all the elements
+ # returned by os.listdir().
+ mocked_os_listdir.return_value = ["a_file"]
+ self.assertRaises(CommandError, retrieve_file,
+ "a_path", ["ext"])
+
+ @patch("os.listdir")
+ def test_retrieve_job_file_1(self, mocked_os_listdir):
+ # Pass some files and directories to retrieve_file(), and make
+ # sure a file with .json suffix is returned.
+ # Pass also a hidden file.
+ try:
+ json_file = tempfile.NamedTemporaryFile(suffix=".json")
+ json_file_name = os.path.basename(json_file.name)
+
+ file_name_no_suffix = tempfile.NamedTemporaryFile(delete=False)
+ file_name_with_suffix = tempfile.NamedTemporaryFile(
+ suffix=".bork", delete=False)
+
+ temp_dir_name = "submit_command_test_tmp_dir"
+ temp_dir_path = os.path.join(tempfile.gettempdir(), temp_dir_name)
+ os.makedirs(temp_dir_path)
+
+ hidden_file = tempfile.NamedTemporaryFile(
+ prefix=".tmp", delete=False)
+
+ mocked_os_listdir.return_value = [
+ temp_dir_name, file_name_no_suffix.name,
+ file_name_with_suffix.name, json_file_name, hidden_file.name]
+
+ obtained = retrieve_file(tempfile.gettempdir(), ["json"])
+ self.assertEqual(json_file.name, obtained)
+ finally:
+ os.removedirs(temp_dir_path)
+ os.unlink(file_name_no_suffix.name)
+ os.unlink(file_name_with_suffix.name)
+ os.unlink(hidden_file.name)
+
+ def test_retrieve_job_file_2(self):
+ # Pass a file with the valid extension.
+ temp_file = tempfile.NamedTemporaryFile(suffix=".json")
+ obtained = retrieve_file(temp_file.name, ["json"])
+ self.assertEquals(temp_file.name, obtained)
+
+ def test_retrieve_job_file_3(self):
+ # Pass a file with a non-valid extension.
+ temp_file = tempfile.NamedTemporaryFile(suffix=".bork")
+ self.assertRaises(
+ CommandError, retrieve_file, temp_file.name, ["json"])
+
+ @patch("os.listdir")
+ def test_retrieve_job_file_4(self, mocked_os_listdir):
+ # Pass hidden and wrong files and make sure exception is thrown.
+ a_hidden_file = ".a_hidden.json"
+ b_hidden_file = ".b_hidden.json"
+ c_wrong_file = "a_wrong_file.bork"
+
+ mocked_os_listdir.return_value = [a_hidden_file, b_hidden_file, c_wrong_file]
+ self.assertRaises(
+ CommandError, retrieve_file, tempfile.gettempdir(), ["json"])
+
+ @patch("lava_tool.utils.subprocess")
+ def test_execute_0(self, mocked_subprocess):
+ mocked_subprocess.check_call = MagicMock()
+ execute("foo")
+ self.assertEqual(mocked_subprocess.check_call.call_args_list,
+ [call(["foo"])])
+ self.assertTrue(mocked_subprocess.check_call.called)
+
+ @patch("lava_tool.utils.subprocess.check_call")
+ def test_execute_1(self, mocked_check_call):
+ mocked_check_call.side_effect = subprocess.CalledProcessError(1, "foo")
+ self.assertRaises(CommandError, execute, ["foo"])
+
+ @patch("lava_tool.utils.subprocess")
+ @patch("lava_tool.utils.has_command", return_value=False)
+ @patch("lava_tool.utils.os.environ.get", return_value=None)
+ @patch("lava_tool.utils.sys.exit")
+ def test_edit_file_0(self, mocked_sys_exit, mocked_env_get,
+ mocked_has_command, mocked_subprocess):
+ edit_file(self.temp_file.name)
+ self.assertTrue(mocked_sys_exit.called)
+
+ @patch("lava_tool.utils.subprocess")
+ @patch("lava_tool.utils.has_command", side_effect=[True, False])
+ @patch("lava_tool.utils.os.environ.get", return_value=None)
+ def test_edit_file_1(self, mocked_env_get, mocked_has_command,
+ mocked_subprocess):
+ mocked_subprocess.Popen = MagicMock()
+ edit_file(self.temp_file.name)
+ expected = [call(["sensible-editor", self.temp_file.name])]
+ self.assertEqual(expected, mocked_subprocess.Popen.call_args_list)
+
+ @patch("lava_tool.utils.subprocess")
+ @patch("lava_tool.utils.has_command", side_effect=[False, True])
+ @patch("lava_tool.utils.os.environ.get", return_value=None)
+ def test_edit_file_2(self, mocked_env_get, mocked_has_command,
+ mocked_subprocess):
+ mocked_subprocess.Popen = MagicMock()
+ edit_file(self.temp_file.name)
+ expected = [call(["xdg-open", self.temp_file.name])]
+ self.assertEqual(expected, mocked_subprocess.Popen.call_args_list)
+
+ @patch("lava_tool.utils.subprocess")
+ @patch("lava_tool.utils.has_command", return_value=False)
+ @patch("lava_tool.utils.os.environ.get", return_value="vim")
+ def test_edit_file_3(self, mocked_env_get, mocked_has_command,
+ mocked_subprocess):
+ mocked_subprocess.Popen = MagicMock()
+ edit_file(self.temp_file.name)
+ expected = [call(["vim", self.temp_file.name])]
+ self.assertEqual(expected, mocked_subprocess.Popen.call_args_list)
+
+ @patch("lava_tool.utils.subprocess")
+ @patch("lava_tool.utils.has_command", return_value=False)
+ @patch("lava_tool.utils.os.environ.get", return_value="vim")
+ def test_edit_file_4(self, mocked_env_get, mocked_has_command,
+ mocked_subprocess):
+ mocked_subprocess.Popen = MagicMock()
+ mocked_subprocess.Popen.side_effect = Exception()
+ self.assertRaises(CommandError, edit_file, self.temp_file.name)
+
+ def test_can_edit_file(self):
+ # Tests the can_edit_file method of the config command.
+ # This is to make sure the device config file is not erased when
+ # checking if it is possible to open it.
+ expected = ("hostname = a_fake_panda02\nconnection_command = \n"
+ "device_type = panda\n")
+
+ with open(self.temp_file.name, "w") as f:
+ f.write(expected)
+
+ self.assertTrue(can_edit_file(self.temp_file.name))
+ obtained = ""
+ with open(self.temp_file.name) as f:
+ obtained = f.read()
+
+ self.assertEqual(expected, obtained)
+
+ def test_verify_and_create_url_0(self):
+ expected = "https://www.example.org/"
+ obtained = verify_and_create_url("www.example.org")
+ self.assertEquals(expected, obtained)
+
+ def test_verify_and_create_url_1(self):
+ expected = "http://www.example.org/"
+ obtained = verify_and_create_url("http://www.example.org")
+ self.assertEquals(expected, obtained)
+
+ def test_verify_and_create_url_2(self):
+ expected = "http://www.example.org/RPC/"
+ obtained = verify_and_create_url("http://www.example.org/RPC")
+ self.assertEquals(expected, obtained)
+
+ def test_verify_and_create_url_3(self):
+ expected = "https://www.example.org/RPC/"
+ obtained = verify_and_create_url("www.example.org/RPC")
+ self.assertEquals(expected, obtained)
+
+ def test_create_dir_0(self):
+ try:
+ temp_dir = os.path.join(tempfile.gettempdir(), "a_dir")
+ create_dir(temp_dir)
+ self.assertTrue(os.path.isdir(temp_dir))
+ finally:
+ shutil.rmtree(temp_dir)
+
+ def test_create_dir_1(self):
+ try:
+ temp_dir = os.path.join(tempfile.gettempdir(), "a_dir")
+ create_dir(temp_dir, "subdir")
+ self.assertTrue(os.path.isdir(os.path.join(temp_dir, "subdir")))
+ finally:
+ shutil.rmtree(temp_dir)
+
+ def test_create_dir_2(self):
+ temp_dir = os.path.join("/", "a_temp_dir")
+ self.assertRaises(CommandError, create_dir, temp_dir)
diff --git a/lava_tool/utils.py b/lava_tool/utils.py
new file mode 100644
index 0000000..b3d5817
--- /dev/null
+++ b/lava_tool/utils.py
@@ -0,0 +1,329 @@
+# 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 StringIO
+import base64
+import os
+import tarfile
+import tempfile
+import types
+import subprocess
+import sys
+import urlparse
+
+from lava.tool.errors import CommandError
+
+
+def has_command(command):
+ """Checks that the given command is available.
+
+ :param command: The name of the command to check availability.
+ """
+ command_available = True
+ try:
+ subprocess.check_call(["which", command],
+ stdout=open(os.path.devnull, 'w'))
+ except subprocess.CalledProcessError:
+ command_available = False
+ return command_available
+
+
+def to_list(value):
+ """Return a list from the passed value.
+
+ :param value: The parameter to turn into a list.
+ """
+ return_value = []
+ if isinstance(value, types.StringType):
+ return_value = [value]
+ else:
+ return_value = list(value)
+ return return_value
+
+
+def create_tar(paths):
+ """Creates a temporary tar file with the provided paths.
+
+ The tar file is not deleted at the end, it has to be delete by who calls
+ this function.
+
+ If just a directory is passed, it will be flattened out: its contents will
+ be added, but not the directory itself.
+
+ :param paths: List of paths to be included in the tar archive.
+ :type list
+ :return The path to the temporary tar file.
+ """
+ paths = to_list(paths)
+ try:
+ temp_tar_file = tempfile.NamedTemporaryFile(suffix=".tar",
+ delete=False)
+ with tarfile.open(temp_tar_file.name, "w") as tar_file:
+ for path in paths:
+ full_path = os.path.abspath(path)
+ if os.path.isfile(full_path):
+ arcname = os.path.basename(full_path)
+ tar_file.add(full_path, arcname=arcname)
+ elif os.path.isdir(full_path):
+ # If we pass a directory, flatten it out.
+ # List its contents, and add them as they are.
+ for element in os.listdir(full_path):
+ arcname = element
+ tar_file.add(os.path.join(full_path, element),
+ arcname=arcname)
+ return temp_tar_file.name
+ except tarfile.TarError:
+ raise CommandError("Error creating the temporary tar archive.")
+
+
+def base64_encode(path):
+ """Encode in base64 the provided file.
+
+ :param path: The path to a file.
+ :return The file content encoded in base64.
+ """
+ if os.path.isfile(path):
+ encoded_content = StringIO.StringIO()
+
+ try:
+ with open(path) as read_file:
+ base64.encode(read_file, encoded_content)
+
+ return encoded_content.getvalue().strip()
+ except IOError:
+ raise CommandError("Cannot read file "
+ "'{0}'.".format(path))
+ else:
+ raise CommandError("Provided path does not exists or is not a file: "
+ "{0}.".format(path))
+
+
+def retrieve_file(path, extensions):
+ """Searches for a file that has one of the supported extensions.
+
+ The path of the first file that matches one of the supported provided
+ extensions will be returned. The files are examined in alphabetical
+ order.
+
+ :param path: Where to look for the file.
+ :param extensions: A list of extensions the file to look for should
+ have.
+ :return The full path of the file.
+ """
+ if os.path.isfile(path):
+ if check_valid_extension(path, extensions):
+ retrieved_path = path
+ else:
+ raise CommandError("The provided file '{0}' is not "
+ "valid: extension not supported.".format(path))
+ else:
+ dir_listing = os.listdir(path)
+ dir_listing.sort()
+
+ for element in dir_listing:
+ if element.startswith("."):
+ continue
+
+ element_path = os.path.join(path, element)
+ if os.path.isdir(element_path):
+ continue
+ elif os.path.isfile(element_path):
+ if check_valid_extension(element_path, extensions):
+ retrieved_path = element_path
+ break
+ else:
+ raise CommandError("No suitable file found in '{0}'".format(path))
+
+ return retrieved_path
+
+
+def check_valid_extension(path, extensions):
+ """Checks that a file has one of the supported extensions.
+
+ :param path: The file to check.
+ :param extensions: A list of supported extensions.
+ """
+ is_valid = False
+
+ local_path, file_name = os.path.split(path)
+ name, full_extension = os.path.splitext(file_name)
+
+ if full_extension:
+ extension = full_extension[1:].strip().lower()
+ if extension in extensions:
+ is_valid = True
+ return is_valid
+
+
+def verify_file_extension(path, default, supported):
+ """Verifies if a file has a supported extensions.
+
+ If the file does not have one, it will add the default extension
+ provided.
+
+ :param path: The path of a file to verify.
+ :param default: The default extension to use.
+ :param supported: A list of supported extensions to check against.
+ :return The path of the file.
+ """
+ full_path, file_name = os.path.split(path)
+ name, extension = os.path.splitext(file_name)
+ if not extension:
+ path = ".".join([path, default])
+ elif extension[1:].lower() not in supported:
+ path = os.path.join(full_path, ".".join([name, default]))
+ return path
+
+
+def verify_path_existance(path):
+ """Verifies if a given path exists on the file system.
+
+ Raises a CommandError in case it exists.
+
+ :param path: The path to verify.
+ """
+ if os.path.exists(path):
+ raise CommandError("{0} already exists.".format(path))
+
+
+def verify_path_non_existance(path):
+ """Verifies if a given path does not exist on the file system.
+
+ Raises a CommandError in case it does not exist.
+
+ :param path: The path to verify.
+ """
+ if not os.path.exists(path):
+ raise CommandError("{0} does not exists.".format(path))
+
+
+def write_file(path, content):
+ """Creates a file with the specified content.
+
+ :param path: The path of the file to write.
+ :param content: What to write in the file.
+ """
+ try:
+ with open(path, "w") as to_write:
+ to_write.write(content)
+ except (OSError, IOError):
+ raise CommandError("Error writing file '{0}'".format(path))
+
+
+def execute(cmd_args):
+ """Executes the supplied command args.
+
+ :param cmd_args: The command, and its optional arguments, to run.
+ :return The command execution return code.
+ """
+ cmd_args = to_list(cmd_args)
+ try:
+ return subprocess.check_call(cmd_args)
+ except subprocess.CalledProcessError:
+ raise CommandError("Error running the following command: "
+ "{0}".format(" ".join(cmd_args)))
+
+
+def can_edit_file(path):
+ """Checks if a file can be opend in write mode.
+
+ :param path: The path to the file.
+ :return True if it is possible to write on the file, False otherwise.
+ """
+ can_edit = True
+ try:
+ fp = open(path, "a")
+ fp.close()
+ except IOError:
+ can_edit = False
+ return can_edit
+
+
+def edit_file(file_to_edit):
+ """Opens the specified file with the default file editor.
+
+ :param file_to_edit: The file to edit.
+ """
+ editor = os.environ.get("EDITOR", None)
+ if editor is None:
+ if has_command("sensible-editor"):
+ editor = "sensible-editor"
+ elif has_command("xdg-open"):
+ editor = "xdg-open"
+ else:
+ # We really do not know how to open a file.
+ print >> sys.stdout, ("Cannot find an editor to open the "
+ "file '{0}'.".format(file_to_edit))
+ print >> sys.stdout, ("Either set the 'EDITOR' environment "
+ "variable, or install 'sensible-editor' "
+ "or 'xdg-open'.")
+ sys.exit(-1)
+ try:
+ subprocess.Popen([editor, file_to_edit]).wait()
+ except Exception:
+ raise CommandError("Error opening the file '{0}' with the "
+ "following editor: {1}.".format(file_to_edit,
+ editor))
+
+
+def verify_and_create_url(endpoint):
+ """Checks that the provided values make a correct URL.
+
+ If the server address does not contain a scheme, by default it will use
+ HTTPS.
+ The endpoint is then added at the URL.
+
+ :param server: A server URL to verify.
+ :return A URL.
+ """
+ url = ""
+ if endpoint:
+ scheme, netloc, path, params, query, fragment = \
+ urlparse.urlparse(endpoint)
+ if not scheme:
+ scheme = "https"
+ if not netloc:
+ netloc, path = path, ""
+
+ url = urlparse.urlunparse(
+ (scheme, netloc, path, params, query, fragment))
+
+ if url[-1:] != "/":
+ url += "/"
+
+ return url
+
+
+def create_dir(path, dir_name=None):
+ """Checks if a directory does not exists, and creates it.
+
+ :param path: The path where the directory should be created.
+ :param dir_name: An optional name for a directory to be created at
+ path (dir_name will be joined with path).
+ :return The path of the created directory."""
+ created_dir = path
+ if dir_name:
+ created_dir = os.path.join(path, dir_name)
+
+ if not os.path.isdir(created_dir):
+ try:
+ os.makedirs(created_dir)
+ except OSError:
+ raise CommandError("Cannot create directory "
+ "'{0}'.".format(created_dir))
+ return created_dir
diff --git a/setup.py b/setup.py
index 8fe8821..7855268 100755
--- a/setup.py
+++ b/setup.py
@@ -19,7 +19,9 @@
# along with lava-tool. If not, see <http://www.gnu.org/licenses/>.
from setuptools import setup, find_packages
+from os.path import dirname, join
+entry_points = open(join(dirname(__file__), 'entry_points.ini')).read()
setup(
name='lava-tool',
@@ -32,69 +34,7 @@ setup(
url='https://launchpad.net/lava-tool',
test_suite='lava_tool.tests.test_suite',
license="LGPLv3",
- entry_points="""
- [console_scripts]
- lava-tool = lava_tool.dispatcher:main
- lava = lava.tool.main:LavaDispatcher.run
- lava-dashboard-tool=lava_dashboard_tool.main:main
- [lava.commands]
- help = lava.tool.commands.help:help
- scheduler = lava_scheduler_tool.commands:scheduler
- dashboard = lava_dashboard_tool.commands:dashboard
- [lava_tool.commands]
- help = lava.tool.commands.help:help
- auth-add = lava_tool.commands.auth:auth_add
- submit-job = lava_scheduler_tool.commands:submit_job
- resubmit-job = lava_scheduler_tool.commands:resubmit_job
- cancel-job = lava_scheduler_tool.commands:cancel_job
- job-output = lava_scheduler_tool.commands:job_output
- backup=lava_dashboard_tool.commands:backup
- bundles=lava_dashboard_tool.commands:bundles
- data_views=lava_dashboard_tool.commands:data_views
- deserialize=lava_dashboard_tool.commands:deserialize
- get=lava_dashboard_tool.commands:get
- make_stream=lava_dashboard_tool.commands:make_stream
- pull=lava_dashboard_tool.commands:pull
- put=lava_dashboard_tool.commands:put
- query_data_view=lava_dashboard_tool.commands:query_data_view
- restore=lava_dashboard_tool.commands:restore
- server_version=lava_dashboard_tool.commands:server_version
- streams=lava_dashboard_tool.commands:streams
- version=lava_dashboard_tool.commands:version
- [lava.scheduler.commands]
- submit-job = lava_scheduler_tool.commands:submit_job
- resubmit-job = lava_scheduler_tool.commands:resubmit_job
- cancel-job = lava_scheduler_tool.commands:cancel_job
- job-output = lava_scheduler_tool.commands:job_output
- [lava.dashboard.commands]
- backup=lava_dashboard_tool.commands:backup
- bundles=lava_dashboard_tool.commands:bundles
- data_views=lava_dashboard_tool.commands:data_views
- deserialize=lava_dashboard_tool.commands:deserialize
- get=lava_dashboard_tool.commands:get
- make_stream=lava_dashboard_tool.commands:make_stream
- pull=lava_dashboard_tool.commands:pull
- put=lava_dashboard_tool.commands:put
- query_data_view=lava_dashboard_tool.commands:query_data_view
- restore=lava_dashboard_tool.commands:restore
- server_version=lava_dashboard_tool.commands:server_version
- streams=lava_dashboard_tool.commands:streams
- version=lava_dashboard_tool.commands:version
- [lava_dashboard_tool.commands]
- backup=lava_dashboard_tool.commands:backup
- bundles=lava_dashboard_tool.commands:bundles
- data_views=lava_dashboard_tool.commands:data_views
- deserialize=lava_dashboard_tool.commands:deserialize
- get=lava_dashboard_tool.commands:get
- make_stream=lava_dashboard_tool.commands:make_stream
- pull=lava_dashboard_tool.commands:pull
- put=lava_dashboard_tool.commands:put
- query_data_view=lava_dashboard_tool.commands:query_data_view
- restore=lava_dashboard_tool.commands:restore
- server_version=lava_dashboard_tool.commands:server_version
- streams=lava_dashboard_tool.commands:streams
- version=lava_dashboard_tool.commands:version
- """,
+ entry_points=entry_points,
classifiers=[
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
@@ -106,11 +46,17 @@ setup(
"Topic :: Software Development :: Testing",
],
install_requires=[
+ 'PyYAML >= 3.10',
'argparse >= 1.1',
+ 'argcomplete >= 0.3',
'keyring',
'json-schema-validator >= 2.0',
- 'versiontools >= 1.3.1'
+ 'versiontools >= 1.3.1',
+ 'pyxdg == 0.25',
],
setup_requires=['versiontools >= 1.3.1'],
- tests_require=['mocker >= 1.0'],
+ tests_require=[
+ 'mocker >= 1.0',
+ 'mock >= 0.7.2'
+ ],
zip_safe=True)