diff options
author | Antonio Terceiro <antonio.terceiro@linaro.org> | 2013-11-18 18:47:24 -0300 |
---|---|---|
committer | Antonio Terceiro <antonio.terceiro@linaro.org> | 2013-11-18 18:47:24 -0300 |
commit | 99ea8ab8541c3144c5e62cec8cdd327dc13c4388 (patch) | |
tree | 67c033d678e6ea7b0223d9608e5bf7dcd4752f20 | |
parent | 49c36d653c19c018e473d7a1a27bdf5cb607c4be (diff) |
Imported Upstream version 0.8upstream/0.8
62 files changed, 5125 insertions, 482 deletions
@@ -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 @@ -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 @@ -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) |