Spack command extensions: error-handling (#13635)
Generally speaking, errors that are encountered when attempting to load command extensions now terminate the running Spack instance. * Added new exceptions `spack.cmd.PythonNameError` and `spack.cmd.CommandNameError`. * New functions `spack.cmd.require_python_name(pname)` and `spack.cmd.require_cmd_name(cname)` check that `pname` and `cname` respectively meet requirements, throwing the appropriate error if not. * `spack.cmd.get_module()` uses `require_cmd_name()` and passes through exceptions from module load attempts. * `spack.cmd.get_command()` uses `require_cmd_name()` and invokes `get_module()` with the correct command-name form rather than the previous (incorrect) Python name. * Added New exceptions `spack.extensions.CommandNotFoundError` and `spack.extensions.ExtensionNamingError`. * `_extension_regexp` has a new leading underscore to indicate expected privacy. * `spack.extensions.extension_name()` raises an `ExtensionNamingError` rather than using `tty.warn()`. * `spack.extensions.load_command_extension()` checks command source existence early and bails out if missing. Also, exceptions raised by `load_module_from_file()` are passed through. * `spack.extensions.get_module()` raises `CommandNotFoundError` as appropriate. * Spack `main()` allows `parser.add_command()` exceptions to cause program end. Tests: * More common boilerplate has been pulled out into fixtures including `sys.modules` dictionary cleanup and resource-managed creation of a simple command extension with specified contents in the source file for a single named command. * "Hello, World!" test now uses a command named `hello-world` instead of `hello` in order to verify correct handling of commands with hyphens. * New tests for: * Missing (or misnamed) command. * Badly-named extension. * Verification that errors encountered during import of a command are propagated upward. Co-authored-by: Massimiliano Culpo <massimiliano.culpo@gmail.com>
This commit is contained in:
parent
d12e588c60
commit
9698907e74
@ -43,11 +43,28 @@ def python_name(cmd_name):
|
|||||||
return cmd_name.replace("-", "_")
|
return cmd_name.replace("-", "_")
|
||||||
|
|
||||||
|
|
||||||
|
def require_python_name(pname):
|
||||||
|
"""Require that the provided name is a valid python name (per
|
||||||
|
python_name()). Useful for checking parameters for function
|
||||||
|
prerequisites."""
|
||||||
|
if python_name(pname) != pname:
|
||||||
|
raise PythonNameError(pname)
|
||||||
|
|
||||||
|
|
||||||
def cmd_name(python_name):
|
def cmd_name(python_name):
|
||||||
"""Convert module name (with ``_``) to command name (with ``-``)."""
|
"""Convert module name (with ``_``) to command name (with ``-``)."""
|
||||||
return python_name.replace('_', '-')
|
return python_name.replace('_', '-')
|
||||||
|
|
||||||
|
|
||||||
|
def require_cmd_name(cname):
|
||||||
|
"""Require that the provided name is a valid command name (per
|
||||||
|
cmd_name()). Useful for checking parameters for function
|
||||||
|
prerequisites.
|
||||||
|
"""
|
||||||
|
if cmd_name(cname) != cname:
|
||||||
|
raise CommandNameError(cname)
|
||||||
|
|
||||||
|
|
||||||
#: global, cached list of all commands -- access through all_commands()
|
#: global, cached list of all commands -- access through all_commands()
|
||||||
_all_commands = None
|
_all_commands = None
|
||||||
|
|
||||||
@ -91,6 +108,7 @@ def get_module(cmd_name):
|
|||||||
cmd_name (str): name of the command for which to get a module
|
cmd_name (str): name of the command for which to get a module
|
||||||
(contains ``-``, not ``_``).
|
(contains ``-``, not ``_``).
|
||||||
"""
|
"""
|
||||||
|
require_cmd_name(cmd_name)
|
||||||
pname = python_name(cmd_name)
|
pname = python_name(cmd_name)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -102,8 +120,6 @@ def get_module(cmd_name):
|
|||||||
tty.debug('Imported {0} from built-in commands'.format(pname))
|
tty.debug('Imported {0} from built-in commands'.format(pname))
|
||||||
except ImportError:
|
except ImportError:
|
||||||
module = spack.extensions.get_module(cmd_name)
|
module = spack.extensions.get_module(cmd_name)
|
||||||
if not module:
|
|
||||||
raise
|
|
||||||
|
|
||||||
attr_setdefault(module, SETUP_PARSER, lambda *args: None) # null-op
|
attr_setdefault(module, SETUP_PARSER, lambda *args: None) # null-op
|
||||||
attr_setdefault(module, DESCRIPTION, "")
|
attr_setdefault(module, DESCRIPTION, "")
|
||||||
@ -116,14 +132,16 @@ def get_module(cmd_name):
|
|||||||
|
|
||||||
|
|
||||||
def get_command(cmd_name):
|
def get_command(cmd_name):
|
||||||
"""Imports the command's function from a module and returns it.
|
"""Imports the command function associated with cmd_name.
|
||||||
|
|
||||||
|
The function's name is derived from cmd_name using python_name().
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
cmd_name (str): name of the command for which to get a module
|
cmd_name (str): name of the command (contains ``-``, not ``_``).
|
||||||
(contains ``-``, not ``_``).
|
|
||||||
"""
|
"""
|
||||||
|
require_cmd_name(cmd_name)
|
||||||
pname = python_name(cmd_name)
|
pname = python_name(cmd_name)
|
||||||
return getattr(get_module(pname), pname)
|
return getattr(get_module(cmd_name), pname)
|
||||||
|
|
||||||
|
|
||||||
def parse_specs(args, **kwargs):
|
def parse_specs(args, **kwargs):
|
||||||
@ -419,6 +437,22 @@ def spack_is_git_repo():
|
|||||||
return os.path.isdir('.git')
|
return os.path.isdir('.git')
|
||||||
|
|
||||||
|
|
||||||
|
class PythonNameError(spack.error.SpackError):
|
||||||
|
"""Exception class thrown for impermissible python names"""
|
||||||
|
def __init__(self, name):
|
||||||
|
self.name = name
|
||||||
|
super(PythonNameError, self).__init__(
|
||||||
|
'{0} is not a permissible Python name.'.format(name))
|
||||||
|
|
||||||
|
|
||||||
|
class CommandNameError(spack.error.SpackError):
|
||||||
|
"""Exception class thrown for impermissible command names"""
|
||||||
|
def __init__(self, name):
|
||||||
|
self.name = name
|
||||||
|
super(CommandNameError, self).__init__(
|
||||||
|
'{0} is not a permissible Spack command name.'.format(name))
|
||||||
|
|
||||||
|
|
||||||
########################################
|
########################################
|
||||||
# argparse types for argument validation
|
# argparse types for argument validation
|
||||||
########################################
|
########################################
|
||||||
|
@ -11,10 +11,17 @@
|
|||||||
import types
|
import types
|
||||||
|
|
||||||
import llnl.util.lang
|
import llnl.util.lang
|
||||||
import llnl.util.tty as tty
|
|
||||||
import spack.config
|
import spack.config
|
||||||
|
import spack.error
|
||||||
|
|
||||||
extension_regexp = re.compile(r'spack-([\w]*)')
|
_extension_regexp = re.compile(r'spack-(\w[-\w]*)$')
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: For consistency we should use spack.cmd.python_name(), but
|
||||||
|
# currently this would create a circular relationship between
|
||||||
|
# spack.cmd and spack.extensions.
|
||||||
|
def _python_name(cmd_name):
|
||||||
|
return cmd_name.replace('-', '_')
|
||||||
|
|
||||||
|
|
||||||
def extension_name(path):
|
def extension_name(path):
|
||||||
@ -24,15 +31,16 @@ def extension_name(path):
|
|||||||
path (str): path where the extension resides
|
path (str): path where the extension resides
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The extension name or None if path doesn't match the format
|
The extension name.
|
||||||
for Spack's extension.
|
|
||||||
|
Raises:
|
||||||
|
ExtensionNamingError: if path does not match the expected format
|
||||||
|
for a Spack command extension.
|
||||||
"""
|
"""
|
||||||
regexp_match = re.search(extension_regexp, os.path.basename(path))
|
regexp_match = re.search(_extension_regexp,
|
||||||
|
os.path.basename(os.path.normpath(path)))
|
||||||
if not regexp_match:
|
if not regexp_match:
|
||||||
msg = "[FOLDER NAMING]"
|
raise ExtensionNamingError(path)
|
||||||
msg += " {0} doesn't match the format for Spack's extensions"
|
|
||||||
tty.warn(msg.format(path))
|
|
||||||
return None
|
|
||||||
return regexp_match.group(1)
|
return regexp_match.group(1)
|
||||||
|
|
||||||
|
|
||||||
@ -40,23 +48,30 @@ def load_command_extension(command, path):
|
|||||||
"""Loads a command extension from the path passed as argument.
|
"""Loads a command extension from the path passed as argument.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
command (str): name of the command
|
command (str): name of the command (contains ``-``, not ``_``).
|
||||||
path (str): base path of the command extension
|
path (str): base path of the command extension
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
A valid module object if the command is found or None
|
A valid module if found and loadable; None if not found. Module
|
||||||
|
loading exceptions are passed through.
|
||||||
"""
|
"""
|
||||||
extension = extension_name(path)
|
extension = _python_name(extension_name(path))
|
||||||
if not extension:
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Compute the name of the module we search, exit early if already imported
|
# Compute the name of the module we search, exit early if already imported
|
||||||
cmd_package = '{0}.{1}.cmd'.format(__name__, extension)
|
cmd_package = '{0}.{1}.cmd'.format(__name__, extension)
|
||||||
python_name = command.replace('-', '_')
|
python_name = _python_name(command)
|
||||||
module_name = '{0}.{1}'.format(cmd_package, python_name)
|
module_name = '{0}.{1}'.format(cmd_package, python_name)
|
||||||
if module_name in sys.modules:
|
if module_name in sys.modules:
|
||||||
return sys.modules[module_name]
|
return sys.modules[module_name]
|
||||||
|
|
||||||
|
# Compute the absolute path of the file to be loaded, along with the
|
||||||
|
# name of the python module where it will be stored
|
||||||
|
cmd_path = os.path.join(path, extension, 'cmd', python_name + '.py')
|
||||||
|
|
||||||
|
# Short circuit if the command source file does not exist
|
||||||
|
if not os.path.exists(cmd_path):
|
||||||
|
return None
|
||||||
|
|
||||||
def ensure_package_creation(name):
|
def ensure_package_creation(name):
|
||||||
package_name = '{0}.{1}'.format(__name__, name)
|
package_name = '{0}.{1}'.format(__name__, name)
|
||||||
if package_name in sys.modules:
|
if package_name in sys.modules:
|
||||||
@ -82,17 +97,10 @@ def ensure_package_creation(name):
|
|||||||
ensure_package_creation(extension)
|
ensure_package_creation(extension)
|
||||||
ensure_package_creation(extension + '.cmd')
|
ensure_package_creation(extension + '.cmd')
|
||||||
|
|
||||||
# Compute the absolute path of the file to be loaded, along with the
|
# TODO: Upon removal of support for Python 2.6 substitute the call
|
||||||
# name of the python module where it will be stored
|
# TODO: below with importlib.import_module(module_name)
|
||||||
cmd_path = os.path.join(path, extension, 'cmd', command + '.py')
|
module = llnl.util.lang.load_module_from_file(module_name, cmd_path)
|
||||||
|
sys.modules[module_name] = module
|
||||||
try:
|
|
||||||
# TODO: Upon removal of support for Python 2.6 substitute the call
|
|
||||||
# TODO: below with importlib.import_module(module_name)
|
|
||||||
module = llnl.util.lang.load_module_from_file(module_name, cmd_path)
|
|
||||||
sys.modules[module_name] = module
|
|
||||||
except (ImportError, IOError):
|
|
||||||
module = None
|
|
||||||
|
|
||||||
return module
|
return module
|
||||||
|
|
||||||
@ -103,9 +111,8 @@ def get_command_paths():
|
|||||||
extension_paths = spack.config.get('config:extensions') or []
|
extension_paths = spack.config.get('config:extensions') or []
|
||||||
|
|
||||||
for path in extension_paths:
|
for path in extension_paths:
|
||||||
extension = extension_name(path)
|
extension = _python_name(extension_name(path))
|
||||||
if extension:
|
command_paths.append(os.path.join(path, extension, 'cmd'))
|
||||||
command_paths.append(os.path.join(path, extension, 'cmd'))
|
|
||||||
|
|
||||||
return command_paths
|
return command_paths
|
||||||
|
|
||||||
@ -144,7 +151,7 @@ def get_module(cmd_name):
|
|||||||
if module:
|
if module:
|
||||||
return module
|
return module
|
||||||
else:
|
else:
|
||||||
return None
|
raise CommandNotFoundError(cmd_name)
|
||||||
|
|
||||||
|
|
||||||
def get_template_dirs():
|
def get_template_dirs():
|
||||||
@ -154,3 +161,23 @@ def get_template_dirs():
|
|||||||
extension_dirs = spack.config.get('config:extensions') or []
|
extension_dirs = spack.config.get('config:extensions') or []
|
||||||
extensions = [os.path.join(x, 'templates') for x in extension_dirs]
|
extensions = [os.path.join(x, 'templates') for x in extension_dirs]
|
||||||
return extensions
|
return extensions
|
||||||
|
|
||||||
|
|
||||||
|
class CommandNotFoundError(spack.error.SpackError):
|
||||||
|
"""Exception class thrown when a requested command is not recognized as
|
||||||
|
such.
|
||||||
|
"""
|
||||||
|
def __init__(self, cmd_name):
|
||||||
|
super(CommandNotFoundError, self).__init__(
|
||||||
|
'{0} is not a recognized Spack command or extension command;'
|
||||||
|
' check with `spack commands`.'.format(cmd_name))
|
||||||
|
|
||||||
|
|
||||||
|
class ExtensionNamingError(spack.error.SpackError):
|
||||||
|
"""Exception class thrown when a configured extension does not follow
|
||||||
|
the expected naming convention.
|
||||||
|
"""
|
||||||
|
def __init__(self, path):
|
||||||
|
super(ExtensionNamingError, self).__init__(
|
||||||
|
'{0} does not match the format for a Spack extension path.'
|
||||||
|
.format(path))
|
||||||
|
@ -736,17 +736,11 @@ def main(argv=None):
|
|||||||
# ensure options on spack command come before everything
|
# ensure options on spack command come before everything
|
||||||
setup_main_options(args)
|
setup_main_options(args)
|
||||||
|
|
||||||
# Try to load the particular command the caller asked for. If there
|
# Try to load the particular command the caller asked for.
|
||||||
# is no module for it, just die.
|
|
||||||
cmd_name = args.command[0]
|
cmd_name = args.command[0]
|
||||||
cmd_name = aliases.get(cmd_name, cmd_name)
|
cmd_name = aliases.get(cmd_name, cmd_name)
|
||||||
|
|
||||||
try:
|
command = parser.add_command(cmd_name)
|
||||||
command = parser.add_command(cmd_name)
|
|
||||||
except ImportError:
|
|
||||||
if spack.config.get('config:debug'):
|
|
||||||
raise
|
|
||||||
tty.die("Unknown command: %s" % args.command[0])
|
|
||||||
|
|
||||||
# Re-parse with the proper sub-parser added.
|
# Re-parse with the proper sub-parser added.
|
||||||
args, unknown = parser.parse_known_args()
|
args, unknown = parser.parse_known_args()
|
||||||
|
29
lib/spack/spack/test/cmd/init_py_functions.py
Normal file
29
lib/spack/spack/test/cmd/init_py_functions.py
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
# Copyright 2013-2019 Lawrence Livermore National Security, LLC and other
|
||||||
|
# Spack Project Developers. See the top-level COPYRIGHT file for details.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
|
||||||
|
import pytest
|
||||||
|
from spack.cmd import require_python_name, python_name, PythonNameError, \
|
||||||
|
require_cmd_name, cmd_name, CommandNameError
|
||||||
|
|
||||||
|
|
||||||
|
def test_require_python_name():
|
||||||
|
"""Python module names should not contain dashes---ensure that
|
||||||
|
require_python_name() raises the appropriate exception if one is
|
||||||
|
detected.
|
||||||
|
"""
|
||||||
|
require_python_name("okey_dokey")
|
||||||
|
with pytest.raises(PythonNameError):
|
||||||
|
require_python_name("okey-dokey")
|
||||||
|
require_python_name(python_name("okey-dokey"))
|
||||||
|
|
||||||
|
|
||||||
|
def test_require_cmd_name():
|
||||||
|
"""By convention, Spack command names should contain dashes rather than
|
||||||
|
underscores---ensure that require_cmd_name() raises the appropriate
|
||||||
|
exception if underscores are detected.
|
||||||
|
"""
|
||||||
|
require_cmd_name("okey-dokey")
|
||||||
|
with pytest.raises(CommandNameError):
|
||||||
|
require_cmd_name("okey_dokey")
|
||||||
|
require_cmd_name(cmd_name("okey_dokey"))
|
@ -5,24 +5,71 @@
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
import contextlib
|
||||||
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
import spack.cmd
|
||||||
import spack.config
|
import spack.config
|
||||||
|
import spack.extensions
|
||||||
import spack.main
|
import spack.main
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
class Extension:
|
||||||
def extension_root(tmpdir):
|
"""Helper class to simplify the creation of simple command extension
|
||||||
root = tmpdir.mkdir('spack-testcommand')
|
directory structures with a conventional format for testing.
|
||||||
root.ensure('testcommand', 'cmd', dir=True)
|
"""
|
||||||
return root
|
def __init__(self, name, root):
|
||||||
|
"""Create a command extension.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (str): The name of the command extension.
|
||||||
|
root (path object): The temporary root for the command extension
|
||||||
|
(e.g. from tmpdir.mkdir()).
|
||||||
|
"""
|
||||||
|
self.name = name
|
||||||
|
self.pname = spack.cmd.python_name(name)
|
||||||
|
self.root = root
|
||||||
|
self.main = self.root.ensure(self.pname, dir=True)
|
||||||
|
self.cmd = self.main.ensure('cmd', dir=True)
|
||||||
|
|
||||||
|
def add_command(self, command_name, contents):
|
||||||
|
"""Add a command to this command extension.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
command_name (str): The name of the command.
|
||||||
|
contents (str): the desired contents of the new command module
|
||||||
|
file."""
|
||||||
|
spack.cmd.require_cmd_name(command_name)
|
||||||
|
python_name = spack.cmd.python_name(command_name)
|
||||||
|
cmd = self.cmd.ensure(python_name + '.py')
|
||||||
|
cmd.write(contents)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture(scope='function')
|
||||||
def hello_world_cmd(extension_root):
|
def extension_creator(tmpdir, config):
|
||||||
"""Simple extension command with code contained in a single file."""
|
"""Create a basic extension command directory structure"""
|
||||||
hello = extension_root.ensure('testcommand', 'cmd', 'hello.py')
|
@contextlib.contextmanager
|
||||||
hello.write("""
|
def _ce(extension_name='testcommand'):
|
||||||
|
root = tmpdir.mkdir('spack-' + extension_name)
|
||||||
|
extension = Extension(extension_name, root)
|
||||||
|
with spack.config.override('config:extensions',
|
||||||
|
[str(extension.root)]):
|
||||||
|
yield extension
|
||||||
|
list_of_modules = list(sys.modules.keys())
|
||||||
|
try:
|
||||||
|
yield _ce
|
||||||
|
finally:
|
||||||
|
to_be_deleted = [x for x in sys.modules if x not in list_of_modules]
|
||||||
|
for module_name in to_be_deleted:
|
||||||
|
del sys.modules[module_name]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope='function')
|
||||||
|
def hello_world_extension(extension_creator):
|
||||||
|
"""Create an extension with a hello-world command."""
|
||||||
|
with extension_creator() as extension:
|
||||||
|
extension.add_command('hello-world', """
|
||||||
description = "hello world extension command"
|
description = "hello world extension command"
|
||||||
section = "test command"
|
section = "test command"
|
||||||
level = "long"
|
level = "long"
|
||||||
@ -31,27 +78,33 @@ def setup_parser(subparser):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def hello(parser, args):
|
def hello_world(parser, args):
|
||||||
print('Hello world!')
|
print('Hello world!')
|
||||||
""")
|
""")
|
||||||
list_of_modules = list(sys.modules.keys())
|
yield extension
|
||||||
with spack.config.override('config:extensions', [str(extension_root)]):
|
|
||||||
yield spack.main.SpackCommand('hello')
|
|
||||||
|
|
||||||
to_be_deleted = [x for x in sys.modules if x not in list_of_modules]
|
|
||||||
for module_name in to_be_deleted:
|
|
||||||
del sys.modules[module_name]
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture(scope='function')
|
||||||
def hello_world_with_module_in_root(extension_root):
|
def hello_world_cmd(hello_world_extension):
|
||||||
"""Extension command with additional code in the root folder."""
|
"""Create and return an invokable "hello-world" extension command."""
|
||||||
extension_root.ensure('testcommand', '__init__.py')
|
yield spack.main.SpackCommand('hello-world')
|
||||||
command_root = extension_root.join('testcommand', 'cmd')
|
|
||||||
hello = command_root.ensure('hello.py')
|
|
||||||
hello.write("""
|
@pytest.fixture(scope='function')
|
||||||
|
def hello_world_with_module_in_root(extension_creator):
|
||||||
|
"""Create a "hello-world" extension command with additional code in the
|
||||||
|
root folder.
|
||||||
|
"""
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def _hwwmir(extension_name=None):
|
||||||
|
with extension_creator(extension_name) \
|
||||||
|
if extension_name else \
|
||||||
|
extension_creator() as extension:
|
||||||
|
# Note that the namespace of the extension is derived from the
|
||||||
|
# fixture.
|
||||||
|
extension.add_command('hello', """
|
||||||
# Test an absolute import
|
# Test an absolute import
|
||||||
from spack.extensions.testcommand.implementation import hello_world
|
from spack.extensions.{ext_pname}.implementation import hello_world
|
||||||
|
|
||||||
# Test a relative import
|
# Test a relative import
|
||||||
from ..implementation import hello_folks
|
from ..implementation import hello_folks
|
||||||
@ -79,33 +132,143 @@ def hello(parser, args):
|
|||||||
hello_folks()
|
hello_folks()
|
||||||
elif args.subcommand == 'global':
|
elif args.subcommand == 'global':
|
||||||
print(global_message)
|
print(global_message)
|
||||||
""")
|
""".format(ext_pname=extension.pname))
|
||||||
implementation = extension_root.ensure('testcommand', 'implementation.py')
|
|
||||||
implementation.write("""
|
extension.main.ensure('__init__.py')
|
||||||
|
implementation \
|
||||||
|
= extension.main.ensure('implementation.py')
|
||||||
|
implementation.write("""
|
||||||
def hello_world():
|
def hello_world():
|
||||||
print('Hello world!')
|
print('Hello world!')
|
||||||
|
|
||||||
def hello_folks():
|
def hello_folks():
|
||||||
print('Hello folks!')
|
print('Hello folks!')
|
||||||
""")
|
""")
|
||||||
list_of_modules = list(sys.modules.keys())
|
yield spack.main.SpackCommand('hello')
|
||||||
with spack.config.override('config:extensions', [str(extension_root)]):
|
|
||||||
yield spack.main.SpackCommand('hello')
|
|
||||||
|
|
||||||
to_be_deleted = [x for x in sys.modules if x not in list_of_modules]
|
yield _hwwmir
|
||||||
for module_name in to_be_deleted:
|
|
||||||
del sys.modules[module_name]
|
|
||||||
|
|
||||||
|
|
||||||
def test_simple_command_extension(hello_world_cmd):
|
def test_simple_command_extension(hello_world_cmd):
|
||||||
|
"""Basic test of a functioning command."""
|
||||||
output = hello_world_cmd()
|
output = hello_world_cmd()
|
||||||
assert 'Hello world!' in output
|
assert 'Hello world!' in output
|
||||||
|
|
||||||
|
|
||||||
def test_command_with_import(hello_world_with_module_in_root):
|
def test_multi_extension_search(hello_world_extension, extension_creator):
|
||||||
output = hello_world_with_module_in_root('world')
|
"""Ensure we can find an extension command even if it's not in the first
|
||||||
assert 'Hello world!' in output
|
place we look.
|
||||||
output = hello_world_with_module_in_root('folks')
|
"""
|
||||||
assert 'Hello folks!' in output
|
|
||||||
output = hello_world_with_module_in_root('global')
|
with extension_creator('testcommand2'):
|
||||||
assert 'bar' in output
|
assert ('Hello world') in spack.main.SpackCommand('hello-world')()
|
||||||
|
|
||||||
|
|
||||||
|
def test_duplicate_module_load(hello_world_cmd, capsys):
|
||||||
|
"""Ensure duplicate module load attempts are successful.
|
||||||
|
|
||||||
|
The command module will already have been loaded once by the
|
||||||
|
hello_world_cmd fixture.
|
||||||
|
"""
|
||||||
|
parser = spack.main.make_argument_parser()
|
||||||
|
args = []
|
||||||
|
hw_cmd = spack.cmd.get_command(hello_world_cmd.command_name)
|
||||||
|
hw_cmd(parser, args)
|
||||||
|
captured = capsys.readouterr()
|
||||||
|
assert captured == ('Hello world!\n', '')
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('extension_name',
|
||||||
|
[None, 'hyphenated-extension'],
|
||||||
|
ids=['simple', 'hyphenated_extension_name'])
|
||||||
|
def test_command_with_import(extension_name, hello_world_with_module_in_root):
|
||||||
|
"""Ensure we can write a functioning command with multiple imported
|
||||||
|
subcommands, including where the extension name contains a hyphen.
|
||||||
|
"""
|
||||||
|
with hello_world_with_module_in_root(extension_name) as hello_world:
|
||||||
|
output = hello_world('world')
|
||||||
|
assert 'Hello world!' in output
|
||||||
|
output = hello_world('folks')
|
||||||
|
assert 'Hello folks!' in output
|
||||||
|
output = hello_world('global')
|
||||||
|
assert 'bar' in output
|
||||||
|
|
||||||
|
|
||||||
|
def test_missing_command():
|
||||||
|
"""Ensure that we raise the expected exception if the desired command is
|
||||||
|
not present.
|
||||||
|
"""
|
||||||
|
with pytest.raises(spack.extensions.CommandNotFoundError):
|
||||||
|
spack.cmd.get_module("no-such-command")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.\
|
||||||
|
parametrize('extension_path,expected_exception',
|
||||||
|
[('/my/bad/extension',
|
||||||
|
spack.extensions.ExtensionNamingError),
|
||||||
|
('', spack.extensions.ExtensionNamingError),
|
||||||
|
('/my/bad/spack--extra-hyphen',
|
||||||
|
spack.extensions.ExtensionNamingError),
|
||||||
|
('/my/good/spack-extension',
|
||||||
|
spack.extensions.CommandNotFoundError),
|
||||||
|
('/my/still/good/spack-extension/',
|
||||||
|
spack.extensions.CommandNotFoundError),
|
||||||
|
('/my/spack-hyphenated-extension',
|
||||||
|
spack.extensions.CommandNotFoundError)],
|
||||||
|
ids=['no_stem', 'vacuous', 'leading_hyphen',
|
||||||
|
'basic_good', 'trailing_slash', 'hyphenated'])
|
||||||
|
def test_extension_naming(extension_path, expected_exception, config):
|
||||||
|
"""Ensure that we are correctly validating configured extension paths
|
||||||
|
for conformity with the rules: the basename should match
|
||||||
|
``spack-<name>``; <name> may have embedded hyphens but not begin with one.
|
||||||
|
"""
|
||||||
|
with spack.config.override('config:extensions', [extension_path]):
|
||||||
|
with pytest.raises(expected_exception):
|
||||||
|
spack.cmd.get_module("no-such-command")
|
||||||
|
|
||||||
|
|
||||||
|
def test_missing_command_function(extension_creator, capsys):
|
||||||
|
"""Ensure we die as expected if a command module does not have the
|
||||||
|
expected command function defined.
|
||||||
|
"""
|
||||||
|
with extension_creator() as extension:
|
||||||
|
extension.\
|
||||||
|
add_command('bad-cmd',
|
||||||
|
"""\ndescription = "Empty command implementation"\n""")
|
||||||
|
with pytest.raises(SystemExit):
|
||||||
|
spack.cmd.get_module('bad-cmd')
|
||||||
|
capture = capsys.readouterr()
|
||||||
|
assert "must define function 'bad_cmd'." in capture[1]
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_command_paths(config):
|
||||||
|
"""Exercise the construction of extension command search paths."""
|
||||||
|
extensions = ('extension-1', 'extension-2')
|
||||||
|
ext_paths = []
|
||||||
|
expected_cmd_paths = []
|
||||||
|
for ext in extensions:
|
||||||
|
ext_path = os.path.join('my', 'path', 'to', 'spack-' + ext)
|
||||||
|
ext_paths.append(ext_path)
|
||||||
|
expected_cmd_paths.append(os.path.join(ext_path,
|
||||||
|
spack.cmd.python_name(ext),
|
||||||
|
'cmd'))
|
||||||
|
|
||||||
|
with spack.config.override('config:extensions', ext_paths):
|
||||||
|
assert spack.extensions.get_command_paths() == expected_cmd_paths
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('command_name,contents,exception',
|
||||||
|
[('bad-cmd', 'from oopsie.daisy import bad\n',
|
||||||
|
ImportError),
|
||||||
|
('bad-cmd', """var = bad_function_call('blech')\n""",
|
||||||
|
NameError),
|
||||||
|
('bad-cmd', ')\n', SyntaxError)],
|
||||||
|
ids=['ImportError', 'NameError', 'SyntaxError'])
|
||||||
|
def test_failing_command(command_name, contents, exception, extension_creator):
|
||||||
|
"""Ensure that the configured command fails to import with the specified
|
||||||
|
error.
|
||||||
|
"""
|
||||||
|
with extension_creator() as extension:
|
||||||
|
extension.add_command(command_name, contents)
|
||||||
|
with pytest.raises(exception):
|
||||||
|
spack.extensions.get_module(command_name)
|
||||||
|
Loading…
Reference in New Issue
Block a user