Source code for spack.test.cmd_extensions

# Copyright 2013-2022 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 contextlib
import os
import sys

import pytest

import spack.cmd
import spack.config
import spack.extensions
import spack.main

is_windows = sys.platform == 'win32'

[docs]class Extension: """Helper class to simplify the creation of simple command extension directory structures with a conventional format for testing. """ 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()). """ = 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)
[docs] 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)
[docs]@pytest.fixture(scope='function') def extension_creator(tmpdir, config): """Create a basic extension command directory structure""" @contextlib.contextmanager 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]
[docs]@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" section = "test command" level = "long" def setup_parser(subparser): pass def hello_world(parser, args): print('Hello world!') """) yield extension
[docs]@pytest.fixture(scope='function') def hello_world_cmd(hello_world_extension): """Create and return an invokable "hello-world" extension command.""" yield spack.main.SpackCommand('hello-world')
[docs]@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 from spack.extensions.{ext_pname}.implementation import hello_world # Test a relative import from ..implementation import hello_folks description = "hello world extension command" section = "test command" level = "long" # Test setting a global variable in setup_parser and retrieving # it in the command global_message = 'foo' def setup_parser(subparser): sp = subparser.add_subparsers(metavar='SUBCOMMAND', dest='subcommand') global global_message sp.add_parser('world', help='Print Hello world!') sp.add_parser('folks', help='Print Hello folks!') sp.add_parser('global', help='Print Hello folks!') global_message = 'bar' def hello(parser, args): if args.subcommand == 'world': hello_world() elif args.subcommand == 'folks': hello_folks() elif args.subcommand == 'global': print(global_message) """.format(ext_pname=extension.pname)) extension.main.ensure('') implementation \ = extension.main.ensure('') implementation.write(""" def hello_world(): print('Hello world!') def hello_folks(): print('Hello folks!') """) yield spack.main.SpackCommand('hello') yield _hwwmir
[docs]def test_simple_command_extension(hello_world_cmd): """Basic test of a functioning command.""" output = hello_world_cmd() assert 'Hello world!' in output
[docs]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', '')
[docs]@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
[docs]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")
[docs]@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")
[docs]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]
[docs]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) path = os.path.join(ext_path, spack.cmd.python_name(ext), 'cmd') path = os.path.abspath(path) expected_cmd_paths.append(path) with spack.config.override('config:extensions', ext_paths): assert spack.extensions.get_command_paths() == expected_cmd_paths
[docs]def test_variable_in_extension_path(config, working_env): """Test variables in extension paths.""" os.environ['_MY_VAR'] = os.path.join('my', 'var') ext_paths = [ os.path.join("~", "${_MY_VAR}", "spack-extension-1") ] # Home env variable is USERPROFILE on Windows home_env = 'USERPROFILE' if is_windows else 'HOME' expected_ext_paths = [ os.path.join(os.environ[home_env], os.environ['_MY_VAR'], "spack-extension-1") ] with spack.config.override('config:extensions', ext_paths): assert spack.extensions.get_extension_paths() == expected_ext_paths
[docs]@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)