Make testing spack commands simpler (#4868)

Adds SpackCommand class allowing Spack commands to be easily in Python

Example usage:

    from spack.main import SpackCommand
    info = SpackCommand('info')
    out, err = info('mpich')
    print(info.returncode)

This allows easier testing of Spack commands.

Also:
* Simplify command tests
* Simplify mocking in command tests.
* Simplify module command test
* Simplify python command test
* Simplify uninstall command test
* Simplify url command test
* SpackCommand uses more compatible output redirection
This commit is contained in:
Todd Gamblin 2017-07-22 21:27:54 -07:00 committed by GitHub
parent c07d93a3e5
commit f159246d1d
13 changed files with 299 additions and 398 deletions

View File

@ -75,7 +75,8 @@ def remove_options(parser, *options):
break break
def get_cmd_function_name(name): def get_python_name(name):
"""Commands can have '-' in their names, unlike Python identifiers."""
return name.replace("-", "_") return name.replace("-", "_")
@ -89,7 +90,7 @@ def get_module(name):
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, "")
fn_name = get_cmd_function_name(name) fn_name = get_python_name(name)
if not hasattr(module, fn_name): if not hasattr(module, fn_name):
tty.die("Command module %s (%s) must define function '%s'." % tty.die("Command module %s (%s) must define function '%s'." %
(module.__name__, module.__file__, fn_name)) (module.__name__, module.__file__, fn_name))
@ -99,7 +100,8 @@ def get_module(name):
def get_command(name): def get_command(name):
"""Imports the command's function from a module and returns it.""" """Imports the command's function from a module and returns it."""
return getattr(get_module(name), get_cmd_function_name(name)) python_name = get_python_name(name)
return getattr(get_module(python_name), python_name)
def parse_specs(args, **kwargs): def parse_specs(args, **kwargs):

View File

@ -34,6 +34,7 @@
import inspect import inspect
import pstats import pstats
import argparse import argparse
import tempfile
import llnl.util.tty as tty import llnl.util.tty as tty
from llnl.util.tty.color import * from llnl.util.tty.color import *
@ -236,10 +237,14 @@ def add_subcommand_group(title, commands):
def add_command(self, name): def add_command(self, name):
"""Add one subcommand to this parser.""" """Add one subcommand to this parser."""
# convert CLI command name to python module name
name = spack.cmd.get_python_name(name)
# lazily initialize any subparsers # lazily initialize any subparsers
if not hasattr(self, 'subparsers'): if not hasattr(self, 'subparsers'):
# remove the dummy "command" argument. # remove the dummy "command" argument.
self._remove_action(self._actions[-1]) if self._actions[-1].dest == 'command':
self._remove_action(self._actions[-1])
self.subparsers = self.add_subparsers(metavar='COMMAND', self.subparsers = self.add_subparsers(metavar='COMMAND',
dest="command") dest="command")
@ -322,7 +327,7 @@ def setup_main_options(args):
def allows_unknown_args(command): def allows_unknown_args(command):
"""This is a basic argument injection test. """Implements really simple argument injection for unknown arguments.
Commands may add an optional argument called "unknown args" to Commands may add an optional argument called "unknown args" to
indicate they can handle unknonwn args, and we'll pass the unknown indicate they can handle unknonwn args, and we'll pass the unknown
@ -334,7 +339,89 @@ def allows_unknown_args(command):
return (argcount == 3 and varnames[2] == 'unknown_args') return (argcount == 3 and varnames[2] == 'unknown_args')
def _invoke_spack_command(command, parser, args, unknown_args):
"""Run a spack command *without* setting spack global options."""
if allows_unknown_args(command):
return_val = command(parser, args, unknown_args)
else:
if unknown_args:
tty.die('unrecognized arguments: %s' % ' '.join(unknown_args))
return_val = command(parser, args)
# Allow commands to return and error code if they want
return 0 if return_val is None else return_val
class SpackCommand(object):
"""Callable object that invokes a spack command (for testing).
Example usage::
install = SpackCommand('install')
install('-v', 'mpich')
Use this to invoke Spack commands directly from Python and check
their stdout and stderr.
"""
def __init__(self, command, fail_on_error=True):
"""Create a new SpackCommand that invokes ``command`` when called."""
self.parser = make_argument_parser()
self.parser.add_command(command)
self.command_name = command
self.command = spack.cmd.get_command(command)
self.fail_on_error = fail_on_error
def __call__(self, *argv):
"""Invoke this SpackCommand.
Args:
argv (list of str): command line arguments.
Returns:
(str, str): output and error as a strings
On return, if ``fail_on_error`` is False, return value of comman
is set in ``returncode`` property. Otherwise, raise an error.
"""
args, unknown = self.parser.parse_known_args(
[self.command_name] + list(argv))
out, err = sys.stdout, sys.stderr
ofd, ofn = tempfile.mkstemp()
efd, efn = tempfile.mkstemp()
try:
sys.stdout = open(ofn, 'w')
sys.stderr = open(efn, 'w')
self.returncode = _invoke_spack_command(
self.command, self.parser, args, unknown)
except SystemExit as e:
self.returncode = e.code
finally:
sys.stdout.flush()
sys.stdout.close()
sys.stderr.flush()
sys.stderr.close()
sys.stdout, sys.stderr = out, err
return_out = open(ofn).read()
return_err = open(efn).read()
os.unlink(ofn)
os.unlink(efn)
if self.fail_on_error and self.returncode != 0:
raise SpackCommandError(
"Command exited with code %d: %s(%s)" % (
self.returncode, self.command_name,
', '.join("'%s'" % a for a in argv)))
return return_out, return_err
def _main(command, parser, args, unknown_args): def _main(command, parser, args, unknown_args):
"""Run a spack command *and* set spack globaloptions."""
# many operations will fail without a working directory. # many operations will fail without a working directory.
set_working_dir() set_working_dir()
@ -345,12 +432,7 @@ def _main(command, parser, args, unknown_args):
# Now actually execute the command # Now actually execute the command
try: try:
if allows_unknown_args(command): return _invoke_spack_command(command, parser, args, unknown_args)
return_val = command(parser, args, unknown_args)
else:
if unknown_args:
tty.die('unrecognized arguments: %s' % ' '.join(unknown_args))
return_val = command(parser, args)
except SpackError as e: except SpackError as e:
e.die() # gracefully die on any SpackErrors e.die() # gracefully die on any SpackErrors
except Exception as e: except Exception as e:
@ -361,9 +443,6 @@ def _main(command, parser, args, unknown_args):
sys.stderr.write('\n') sys.stderr.write('\n')
tty.die("Keyboard interrupt.") tty.die("Keyboard interrupt.")
# Allow commands to return and error code if they want
return 0 if return_val is None else return_val
def _profile_wrapper(command, parser, args, unknown_args): def _profile_wrapper(command, parser, args, unknown_args):
import cProfile import cProfile
@ -431,7 +510,7 @@ def main(argv=None):
# Try to load the particular command the caller asked for. If there # Try to load the particular command the caller asked for. If there
# is no module for it, just die. # is no module for it, just die.
command_name = args.command[0].replace('-', '_') command_name = spack.cmd.get_python_name(args.command[0])
try: try:
parser.add_command(command_name) parser.add_command(command_name)
except ImportError: except ImportError:
@ -465,3 +544,7 @@ def main(argv=None):
except SystemExit as e: except SystemExit as e:
return e.code return e.code
class SpackCommandError(Exception):
"""Raised when SpackCommand execution fails."""

View File

@ -22,13 +22,12 @@
# License along with this program; if not, write to the Free Software # License along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
############################################################################## ##############################################################################
import argparse
import os import os
import pytest import pytest
import spack import spack
import spack.cmd.gpg as gpg
import spack.util.gpg as gpg_util import spack.util.gpg as gpg_util
from spack.main import SpackCommand
from spack.util.executable import ProcessError from spack.util.executable import ProcessError
@ -40,6 +39,19 @@ def testing_gpg_directory(tmpdir):
gpg_util.GNUPGHOME = old_gpg_path gpg_util.GNUPGHOME = old_gpg_path
@pytest.fixture(scope='function')
def mock_gpg_config():
orig_gpg_keys_path = spack.gpg_keys_path
spack.gpg_keys_path = spack.mock_gpg_keys_path
yield
spack.gpg_keys_path = orig_gpg_keys_path
@pytest.fixture(scope='function')
def gpg():
return SpackCommand('gpg')
def has_gnupg2(): def has_gnupg2():
try: try:
gpg_util.Gpg.gpg()('--version', output=os.devnull) gpg_util.Gpg.gpg()('--version', output=os.devnull)
@ -48,45 +60,31 @@ def has_gnupg2():
return False return False
@pytest.mark.usefixtures('testing_gpg_directory') @pytest.mark.xfail # TODO: fix failing tests.
@pytest.mark.skipif(not has_gnupg2(), @pytest.mark.skipif(not has_gnupg2(),
reason='These tests require gnupg2') reason='These tests require gnupg2')
def test_gpg(tmpdir): def test_gpg(gpg, tmpdir, testing_gpg_directory, mock_gpg_config):
parser = argparse.ArgumentParser()
gpg.setup_parser(parser)
# Verify a file with an empty keyring. # Verify a file with an empty keyring.
args = parser.parse_args(['verify', os.path.join(
spack.mock_gpg_data_path, 'content.txt')])
with pytest.raises(ProcessError): with pytest.raises(ProcessError):
gpg.gpg(parser, args) gpg('verify', os.path.join(spack.mock_gpg_data_path, 'content.txt'))
# Import the default key. # Import the default key.
args = parser.parse_args(['init']) gpg('init')
args.import_dir = spack.mock_gpg_keys_path
gpg.gpg(parser, args)
# List the keys. # List the keys.
# TODO: Test the output here. # TODO: Test the output here.
args = parser.parse_args(['list', '--trusted']) gpg('list', '--trusted')
gpg.gpg(parser, args) gpg('list', '--signing')
args = parser.parse_args(['list', '--signing'])
gpg.gpg(parser, args)
# Verify the file now that the key has been trusted. # Verify the file now that the key has been trusted.
args = parser.parse_args(['verify', os.path.join( gpg('verify', os.path.join(spack.mock_gpg_data_path, 'content.txt'))
spack.mock_gpg_data_path, 'content.txt')])
gpg.gpg(parser, args)
# Untrust the default key. # Untrust the default key.
args = parser.parse_args(['untrust', 'Spack testing']) gpg('untrust', 'Spack testing')
gpg.gpg(parser, args)
# Now that the key is untrusted, verification should fail. # Now that the key is untrusted, verification should fail.
args = parser.parse_args(['verify', os.path.join(
spack.mock_gpg_data_path, 'content.txt')])
with pytest.raises(ProcessError): with pytest.raises(ProcessError):
gpg.gpg(parser, args) gpg('verify', os.path.join(spack.mock_gpg_data_path, 'content.txt'))
# Create a file to test signing. # Create a file to test signing.
test_path = tmpdir.join('to-sign.txt') test_path = tmpdir.join('to-sign.txt')
@ -94,88 +92,71 @@ def test_gpg(tmpdir):
fout.write('Test content for signing.\n') fout.write('Test content for signing.\n')
# Signing without a private key should fail. # Signing without a private key should fail.
args = parser.parse_args(['sign', str(test_path)])
with pytest.raises(RuntimeError) as exc_info: with pytest.raises(RuntimeError) as exc_info:
gpg.gpg(parser, args) gpg('sign', str(test_path))
assert exc_info.value.args[0] == 'no signing keys are available' assert exc_info.value.args[0] == 'no signing keys are available'
# Create a key for use in the tests. # Create a key for use in the tests.
keypath = tmpdir.join('testing-1.key') keypath = tmpdir.join('testing-1.key')
args = parser.parse_args(['create', gpg('create',
'--comment', 'Spack testing key', '--comment', 'Spack testing key',
'--export', str(keypath), '--export', str(keypath),
'Spack testing 1', 'Spack testing 1',
'spack@googlegroups.com']) 'spack@googlegroups.com')
gpg.gpg(parser, args)
keyfp = gpg_util.Gpg.signing_keys()[0] keyfp = gpg_util.Gpg.signing_keys()[0]
# List the keys. # List the keys.
# TODO: Test the output here. # TODO: Test the output here.
args = parser.parse_args(['list', '--trusted']) gpg('list', '--trusted')
gpg.gpg(parser, args) gpg('list', '--signing')
args = parser.parse_args(['list', '--signing'])
gpg.gpg(parser, args)
# Signing with the default (only) key. # Signing with the default (only) key.
args = parser.parse_args(['sign', str(test_path)]) gpg('sign', str(test_path))
gpg.gpg(parser, args)
# Verify the file we just verified. # Verify the file we just verified.
args = parser.parse_args(['verify', str(test_path)]) gpg('verify', str(test_path))
gpg.gpg(parser, args)
# Export the key for future use. # Export the key for future use.
export_path = tmpdir.join('export.testing.key') export_path = tmpdir.join('export.testing.key')
args = parser.parse_args(['export', str(export_path)]) gpg('export', str(export_path))
gpg.gpg(parser, args)
# Create a second key for use in the tests. # Create a second key for use in the tests.
args = parser.parse_args(['create', gpg('create',
'--comment', 'Spack testing key', '--comment', 'Spack testing key',
'Spack testing 2', 'Spack testing 2',
'spack@googlegroups.com']) 'spack@googlegroups.com')
gpg.gpg(parser, args)
# List the keys. # List the keys.
# TODO: Test the output here. # TODO: Test the output here.
args = parser.parse_args(['list', '--trusted']) gpg('list', '--trusted')
gpg.gpg(parser, args) gpg('list', '--signing')
args = parser.parse_args(['list', '--signing'])
gpg.gpg(parser, args)
test_path = tmpdir.join('to-sign-2.txt') test_path = tmpdir.join('to-sign-2.txt')
with open(str(test_path), 'w+') as fout: with open(str(test_path), 'w+') as fout:
fout.write('Test content for signing.\n') fout.write('Test content for signing.\n')
# Signing with multiple signing keys is ambiguous. # Signing with multiple signing keys is ambiguous.
args = parser.parse_args(['sign', str(test_path)])
with pytest.raises(RuntimeError) as exc_info: with pytest.raises(RuntimeError) as exc_info:
gpg.gpg(parser, args) gpg('sign', str(test_path))
assert exc_info.value.args[0] == \ assert exc_info.value.args[0] == \
'multiple signing keys are available; please choose one' 'multiple signing keys are available; please choose one'
# Signing with a specified key. # Signing with a specified key.
args = parser.parse_args(['sign', '--key', keyfp, str(test_path)]) gpg('sign', '--key', keyfp, str(test_path))
gpg.gpg(parser, args)
# Untrusting signing keys needs a flag. # Untrusting signing keys needs a flag.
args = parser.parse_args(['untrust', 'Spack testing 1'])
with pytest.raises(ProcessError): with pytest.raises(ProcessError):
gpg.gpg(parser, args) gpg('untrust', 'Spack testing 1')
# Untrust the key we created. # Untrust the key we created.
args = parser.parse_args(['untrust', '--signing', keyfp]) gpg('untrust', '--signing', keyfp)
gpg.gpg(parser, args)
# Verification should now fail. # Verification should now fail.
args = parser.parse_args(['verify', str(test_path)])
with pytest.raises(ProcessError): with pytest.raises(ProcessError):
gpg.gpg(parser, args) gpg('verify', str(test_path))
# Trust the exported key. # Trust the exported key.
args = parser.parse_args(['trust', str(export_path)]) gpg('trust', str(export_path))
gpg.gpg(parser, args)
# Verification should now succeed again. # Verification should now succeed again.
args = parser.parse_args(['verify', str(test_path)]) gpg('verify', str(test_path))
gpg.gpg(parser, args)

View File

@ -22,194 +22,45 @@
# License along with this program; if not, write to the Free Software # License along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
############################################################################## ##############################################################################
import argparse from spack.main import SpackCommand
import codecs
import collections
import contextlib
import unittest
from six import StringIO
import llnl.util.filesystem
import spack
import spack.cmd
import spack.cmd.install as install
FILE_REGISTRY = collections.defaultdict(StringIO)
# Monkey-patch open to write module files to a StringIO instance install = SpackCommand('install')
@contextlib.contextmanager
def mock_open(filename, mode, *args):
if not mode == 'wb':
message = 'test.test_install : unexpected opening mode for mock_open'
raise RuntimeError(message)
FILE_REGISTRY[filename] = StringIO()
try:
yield FILE_REGISTRY[filename]
finally:
handle = FILE_REGISTRY[filename]
FILE_REGISTRY[filename] = handle.getvalue()
handle.close()
class MockSpec(object): def _install_package_and_dependency(
tmpdir, builtin_mock, mock_archive, mock_fetch, config,
install_mockery):
def __init__(self, name, version, hashStr=None): tmpdir.chdir()
self._dependencies = {} install('--log-format=junit', '--log-file=test.xml', 'libdwarf')
self.name = name
self.version = version
self.hash = hashStr if hashStr else hash((name, version))
def _deptype_norm(self, deptype): files = tmpdir.listdir()
if deptype is None: filename = tmpdir.join('test.xml')
return spack.alldeps assert filename in files
# Force deptype to be a tuple so that we can do set intersections.
if isinstance(deptype, str):
return (deptype,)
return deptype
def _find_deps(self, where, deptype): content = filename.open().read()
deptype = self._deptype_norm(deptype) assert 'tests="2"' in content
assert 'failures="0"' in content
return [dep.spec assert 'errors="0"' in content
for dep in where.values()
if deptype and any(d in deptype for d in dep.deptypes)]
def dependencies(self, deptype=None):
return self._find_deps(self._dependencies, deptype)
def dependents(self, deptype=None):
return self._find_deps(self._dependents, deptype)
def traverse(self, order=None):
for _, spec in self._dependencies.items():
yield spec.spec
yield self
def dag_hash(self):
return self.hash
@property
def short_spec(self):
return '-'.join([self.name, str(self.version), str(self.hash)])
class MockPackage(object): def test_install_package_already_installed(
tmpdir, builtin_mock, mock_archive, mock_fetch, config,
install_mockery):
def __init__(self, spec, buildLogPath): tmpdir.chdir()
self.name = spec.name install('libdwarf')
self.spec = spec install('--log-format=junit', '--log-file=test.xml', 'libdwarf')
self.installed = False
self.build_log_path = buildLogPath
def do_install(self, *args, **kwargs): files = tmpdir.listdir()
for x in self.spec.dependencies(): filename = tmpdir.join('test.xml')
x.package.do_install(*args, **kwargs) assert filename in files
self.installed = True
content = filename.open().read()
assert 'tests="2"' in content
assert 'failures="0"' in content
assert 'errors="0"' in content
class MockPackageDb(object): skipped = [line for line in content.split('\n') if 'skipped' in line]
assert len(skipped) == 2
def __init__(self, init=None):
self.specToPkg = {}
if init:
self.specToPkg.update(init)
def get(self, spec):
return self.specToPkg[spec]
def mock_fetch_log(path):
return []
specX = MockSpec('X', '1.2.0')
specY = MockSpec('Y', '2.3.8')
specX._dependencies['Y'] = spack.spec.DependencySpec(
specX, specY, spack.alldeps)
pkgX = MockPackage(specX, 'logX')
pkgY = MockPackage(specY, 'logY')
specX.package = pkgX
specY.package = pkgY
# TODO: add test(s) where Y fails to install
class InstallTestJunitLog(unittest.TestCase):
"""Tests test-install where X->Y"""
def setUp(self):
super(InstallTestJunitLog, self).setUp()
install.PackageBase = MockPackage
# Monkey patch parse specs
def monkey_parse_specs(x, concretize):
if x == ['X']:
return [specX]
elif x == ['Y']:
return [specY]
return []
self.parse_specs = spack.cmd.parse_specs
spack.cmd.parse_specs = monkey_parse_specs
# Monkey patch os.mkdirp
self.mkdirp = llnl.util.filesystem.mkdirp
llnl.util.filesystem.mkdirp = lambda x: True
# Monkey patch open
self.codecs_open = codecs.open
codecs.open = mock_open
# Clean FILE_REGISTRY
FILE_REGISTRY.clear()
pkgX.installed = False
pkgY.installed = False
# Monkey patch pkgDb
self.saved_db = spack.repo
pkgDb = MockPackageDb({specX: pkgX, specY: pkgY})
spack.repo = pkgDb
def tearDown(self):
# Remove the monkey patched test_install.open
codecs.open = self.codecs_open
# Remove the monkey patched os.mkdir
llnl.util.filesystem.mkdirp = self.mkdirp
del self.mkdirp
# Remove the monkey patched parse_specs
spack.cmd.parse_specs = self.parse_specs
del self.parse_specs
super(InstallTestJunitLog, self).tearDown()
spack.repo = self.saved_db
def test_installing_both(self):
parser = argparse.ArgumentParser()
install.setup_parser(parser)
args = parser.parse_args(['--log-format=junit', 'X'])
install.install(parser, args)
self.assertEqual(len(FILE_REGISTRY), 1)
for _, content in FILE_REGISTRY.items():
self.assertTrue('tests="2"' in content)
self.assertTrue('failures="0"' in content)
self.assertTrue('errors="0"' in content)
def test_dependency_already_installed(self):
pkgX.installed = True
pkgY.installed = True
parser = argparse.ArgumentParser()
install.setup_parser(parser)
args = parser.parse_args(['--log-format=junit', 'X'])
install.install(parser, args)
self.assertEqual(len(FILE_REGISTRY), 1)
for _, content in FILE_REGISTRY.items():
self.assertTrue('tests="2"' in content)
self.assertTrue('failures="0"' in content)
self.assertTrue('errors="0"' in content)
self.assertEqual(
sum('skipped' in line for line in content.split('\n')), 2)

View File

@ -70,15 +70,20 @@ def test_remove_and_add_tcl(database, parser):
# Remove existing modules [tcl] # Remove existing modules [tcl]
args = parser.parse_args(['rm', '-y', 'mpileaks']) args = parser.parse_args(['rm', '-y', 'mpileaks'])
module_files = _get_module_files(args) module_files = _get_module_files(args)
for item in module_files: for item in module_files:
assert os.path.exists(item) assert os.path.exists(item)
module.module(parser, args) module.module(parser, args)
for item in module_files: for item in module_files:
assert not os.path.exists(item) assert not os.path.exists(item)
# Add them back [tcl] # Add them back [tcl]
args = parser.parse_args(['refresh', '-y', 'mpileaks']) args = parser.parse_args(['refresh', '-y', 'mpileaks'])
module.module(parser, args) module.module(parser, args)
for item in module_files: for item in module_files:
assert os.path.exists(item) assert os.path.exists(item)

View File

@ -22,22 +22,12 @@
# License along with this program; if not, write to the Free Software # License along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
############################################################################## ##############################################################################
import argparse import spack
import pytest from spack.main import SpackCommand
from spack.cmd.python import * python = SpackCommand('python')
@pytest.fixture(scope='module') def test_python():
def parser(): out, err = python('-c', 'import spack; print(spack.spack_version)')
"""Returns the parser for the ``python`` command""" assert out.strip() == str(spack.spack_version)
parser = argparse.ArgumentParser()
setup_parser(parser)
return parser
def test_python(parser):
args = parser.parse_args([
'-c', 'import spack; print(spack.spack_version)'
])
python(parser, args)

View File

@ -24,7 +24,9 @@
############################################################################## ##############################################################################
import pytest import pytest
import spack.store import spack.store
import spack.cmd.uninstall from spack.main import SpackCommand, SpackCommandError
uninstall = SpackCommand('uninstall')
class MockArgs(object): class MockArgs(object):
@ -37,20 +39,21 @@ def __init__(self, packages, all=False, force=False, dependents=False):
self.yes_to_all = True self.yes_to_all = True
def test_uninstall(database): def test_multiple_matches(database):
parser = None """Test unable to uninstall when multiple matches."""
uninstall = spack.cmd.uninstall.uninstall with pytest.raises(SpackCommandError):
# Multiple matches uninstall('-y', 'mpileaks')
args = MockArgs(['mpileaks'])
with pytest.raises(SystemExit):
uninstall(parser, args) def test_installed_dependents(database):
# Installed dependents """Test can't uninstall when ther are installed dependents."""
args = MockArgs(['libelf']) with pytest.raises(SpackCommandError):
with pytest.raises(SystemExit): uninstall('-y', 'libelf')
uninstall(parser, args)
# Recursive uninstall
args = MockArgs(['callpath'], all=True, dependents=True) def test_recursive_uninstall(database):
uninstall(parser, args) """Test recursive uninstall."""
uninstall('-y', '-a', '--dependents', 'callpath')
all_specs = spack.store.layout.all_specs() all_specs = spack.store.layout.all_specs()
assert len(all_specs) == 8 assert len(all_specs) == 8

View File

@ -22,18 +22,13 @@
# License along with this program; if not, write to the Free Software # License along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
############################################################################## ##############################################################################
import argparse import re
import pytest import pytest
from spack.url import UndetectableVersionError
from spack.main import SpackCommand
from spack.cmd.url import * from spack.cmd.url import *
url = SpackCommand('url')
@pytest.fixture(scope='module')
def parser():
"""Returns the parser for the ``url`` command"""
parser = argparse.ArgumentParser()
setup_parser(parser)
return parser
class MyPackage: class MyPackage:
@ -77,51 +72,64 @@ def test_version_parsed_correctly():
assert not version_parsed_correctly(MyPackage('', ['0.18.0']), 'oce-0.18.0') # noqa assert not version_parsed_correctly(MyPackage('', ['0.18.0']), 'oce-0.18.0') # noqa
def test_url_parse(parser): def test_url_parse():
args = parser.parse_args(['parse', 'http://zlib.net/fossils/zlib-1.2.10.tar.gz']) url('parse', 'http://zlib.net/fossils/zlib-1.2.10.tar.gz')
url(parser, args)
@pytest.mark.xfail def test_url_with_no_version_fails():
def test_url_parse_xfail(parser):
# No version in URL # No version in URL
args = parser.parse_args(['parse', 'http://www.netlib.org/voronoi/triangle.zip']) with pytest.raises(UndetectableVersionError):
url(parser, args) url('parse', 'http://www.netlib.org/voronoi/triangle.zip')
def test_url_list(parser): def test_url_list():
args = parser.parse_args(['list']) out, err = url('list')
total_urls = url_list(args) total_urls = len(out.split('\n'))
# The following two options should not change the number of URLs printed. # The following two options should not change the number of URLs printed.
args = parser.parse_args(['list', '--color', '--extrapolation']) out, err = url('list', '--color', '--extrapolation')
colored_urls = url_list(args) colored_urls = len(out.split('\n'))
assert colored_urls == total_urls assert colored_urls == total_urls
# The following options should print fewer URLs than the default. # The following options should print fewer URLs than the default.
# If they print the same number of URLs, something is horribly broken. # If they print the same number of URLs, something is horribly broken.
# If they say we missed 0 URLs, something is probably broken too. # If they say we missed 0 URLs, something is probably broken too.
args = parser.parse_args(['list', '--incorrect-name']) out, err = url('list', '--incorrect-name')
incorrect_name_urls = url_list(args) incorrect_name_urls = len(out.split('\n'))
assert 0 < incorrect_name_urls < total_urls assert 0 < incorrect_name_urls < total_urls
args = parser.parse_args(['list', '--incorrect-version']) out, err = url('list', '--incorrect-version')
incorrect_version_urls = url_list(args) incorrect_version_urls = len(out.split('\n'))
assert 0 < incorrect_version_urls < total_urls assert 0 < incorrect_version_urls < total_urls
args = parser.parse_args(['list', '--correct-name']) out, err = url('list', '--correct-name')
correct_name_urls = url_list(args) correct_name_urls = len(out.split('\n'))
assert 0 < correct_name_urls < total_urls assert 0 < correct_name_urls < total_urls
args = parser.parse_args(['list', '--correct-version']) out, err = url('list', '--correct-version')
correct_version_urls = url_list(args) correct_version_urls = len(out.split('\n'))
assert 0 < correct_version_urls < total_urls assert 0 < correct_version_urls < total_urls
def test_url_summary(parser): def test_url_summary():
args = parser.parse_args(['summary']) """Test the URL summary command."""
# test url_summary, the internal function that does the work
(total_urls, correct_names, correct_versions, (total_urls, correct_names, correct_versions,
name_count_dict, version_count_dict) = url_summary(args) name_count_dict, version_count_dict) = url_summary(None)
assert 0 < correct_names <= sum(name_count_dict.values()) <= total_urls # noqa assert 0 < correct_names <= sum(name_count_dict.values()) <= total_urls # noqa
assert 0 < correct_versions <= sum(version_count_dict.values()) <= total_urls # noqa assert 0 < correct_versions <= sum(version_count_dict.values()) <= total_urls # noqa
# make sure it agrees with the actual command.
out, err = url('summary')
out_total_urls = int(
re.search(r'Total URLs found:\s*(\d+)', out).group(1))
assert out_total_urls == total_urls
out_correct_names = int(
re.search(r'Names correctly parsed:\s*(\d+)', out).group(1))
assert out_correct_names == correct_names
out_correct_versions = int(
re.search(r'Versions correctly parsed:\s*(\d+)', out).group(1))
assert out_correct_versions == correct_versions

View File

@ -35,16 +35,18 @@
import py import py
import pytest import pytest
import spack import spack
import spack.architecture import spack.architecture
import spack.database import spack.database
import spack.directory_layout import spack.directory_layout
import spack.fetch_strategy
import spack.platforms.test import spack.platforms.test
import spack.repository import spack.repository
import spack.stage import spack.stage
import spack.util.executable import spack.util.executable
import spack.util.pattern import spack.util.pattern
from spack.package import PackageBase
from spack.fetch_strategy import *
########## ##########
@ -78,12 +80,10 @@ def set_stage(self, stage):
pass pass
def fetch(self): def fetch(self):
raise spack.fetch_strategy.FetchError( raise FetchError('Mock cache always fails for tests')
'Mock cache always fails for tests'
)
def __str__(self): def __str__(self):
return "[mock fetcher]" return "[mock fetch cache]"
monkeypatch.setattr(spack, 'fetch_cache', MockCache()) monkeypatch.setattr(spack, 'fetch_cache', MockCache())
@ -287,6 +287,43 @@ def refresh_db_on_exit(database):
yield yield
database.refresh() database.refresh()
@pytest.fixture()
def install_mockery(tmpdir, config, builtin_mock):
"""Hooks a fake install directory and a fake db into Spack."""
layout = spack.store.layout
db = spack.store.db
# Use a fake install directory to avoid conflicts bt/w
# installed pkgs and mock packages.
spack.store.layout = spack.directory_layout.YamlDirectoryLayout(
str(tmpdir))
spack.store.db = spack.database.Database(str(tmpdir))
# We use a fake package, so skip the checksum.
spack.do_checksum = False
yield
# Turn checksumming back on
spack.do_checksum = True
# Restore Spack's layout.
spack.store.layout = layout
spack.store.db = db
@pytest.fixture()
def mock_fetch(mock_archive):
"""Fake the URL for a package so it downloads from a file."""
fetcher = FetchStrategyComposite()
fetcher.append(URLFetchStrategy(mock_archive.url))
@property
def fake_fn(self):
return fetcher
orig_fn = PackageBase.fetcher
PackageBase.fetcher = fake_fn
yield
PackageBase.fetcher = orig_fn
########## ##########
# Fake archives and repositories # Fake archives and repositories
########## ##########

View File

@ -22,45 +22,15 @@
# License along with this program; if not, write to the Free Software # License along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
############################################################################## ##############################################################################
import os
import pytest import pytest
import spack import spack
import spack.store import spack.store
from spack.database import Database
from spack.directory_layout import YamlDirectoryLayout
from spack.fetch_strategy import URLFetchStrategy, FetchStrategyComposite
from spack.spec import Spec from spack.spec import Spec
import os
def test_install_and_uninstall(install_mockery, mock_fetch):
@pytest.fixture()
def install_mockery(tmpdir, config, builtin_mock):
"""Hooks a fake install directory and a fake db into Spack."""
layout = spack.store.layout
db = spack.store.db
# Use a fake install directory to avoid conflicts bt/w
# installed pkgs and mock packages.
spack.store.layout = YamlDirectoryLayout(str(tmpdir))
spack.store.db = Database(str(tmpdir))
# We use a fake package, so skip the checksum.
spack.do_checksum = False
yield
# Turn checksumming back on
spack.do_checksum = True
# Restore Spack's layout.
spack.store.layout = layout
spack.store.db = db
def fake_fetchify(url, pkg):
"""Fake the URL for a package so it downloads from a file."""
fetcher = FetchStrategyComposite()
fetcher.append(URLFetchStrategy(url))
pkg.fetcher = fetcher
@pytest.mark.usefixtures('install_mockery')
def test_install_and_uninstall(mock_archive):
# Get a basic concrete spec for the trivial install package. # Get a basic concrete spec for the trivial install package.
spec = Spec('trivial-install-test-package') spec = Spec('trivial-install-test-package')
spec.concretize() spec.concretize()
@ -69,8 +39,6 @@ def test_install_and_uninstall(mock_archive):
# Get the package # Get the package
pkg = spack.repo.get(spec) pkg = spack.repo.get(spec)
fake_fetchify(mock_archive.url, pkg)
try: try:
pkg.do_install() pkg.do_install()
pkg.do_uninstall() pkg.do_uninstall()
@ -114,12 +82,10 @@ def __getattr__(self, attr):
return getattr(self.wrapped_stage, attr) return getattr(self.wrapped_stage, attr)
@pytest.mark.usefixtures('install_mockery') def test_partial_install_delete_prefix_and_stage(install_mockery, mock_fetch):
def test_partial_install_delete_prefix_and_stage(mock_archive):
spec = Spec('canfail') spec = Spec('canfail')
spec.concretize() spec.concretize()
pkg = spack.repo.get(spec) pkg = spack.repo.get(spec)
fake_fetchify(mock_archive.url, pkg)
remove_prefix = spack.package.Package.remove_prefix remove_prefix = spack.package.Package.remove_prefix
instance_rm_prefix = pkg.remove_prefix instance_rm_prefix = pkg.remove_prefix
@ -145,14 +111,12 @@ def test_partial_install_delete_prefix_and_stage(mock_archive):
pass pass
@pytest.mark.usefixtures('install_mockery') def test_partial_install_keep_prefix(install_mockery, mock_fetch):
def test_partial_install_keep_prefix(mock_archive):
spec = Spec('canfail') spec = Spec('canfail')
spec.concretize() spec.concretize()
pkg = spack.repo.get(spec) pkg = spack.repo.get(spec)
# Normally the stage should start unset, but other tests set it # Normally the stage should start unset, but other tests set it
pkg._stage = None pkg._stage = None
fake_fetchify(mock_archive.url, pkg)
remove_prefix = spack.package.Package.remove_prefix remove_prefix = spack.package.Package.remove_prefix
try: try:
# If remove_prefix is called at any point in this test, that is an # If remove_prefix is called at any point in this test, that is an
@ -175,12 +139,10 @@ def test_partial_install_keep_prefix(mock_archive):
pass pass
@pytest.mark.usefixtures('install_mockery') def test_second_install_no_overwrite_first(install_mockery, mock_fetch):
def test_second_install_no_overwrite_first(mock_archive):
spec = Spec('canfail') spec = Spec('canfail')
spec.concretize() spec.concretize()
pkg = spack.repo.get(spec) pkg = spack.repo.get(spec)
fake_fetchify(mock_archive.url, pkg)
remove_prefix = spack.package.Package.remove_prefix remove_prefix = spack.package.Package.remove_prefix
try: try:
spack.package.Package.remove_prefix = mock_remove_prefix spack.package.Package.remove_prefix = mock_remove_prefix
@ -198,28 +160,14 @@ def test_second_install_no_overwrite_first(mock_archive):
pass pass
@pytest.mark.usefixtures('install_mockery') def test_store(install_mockery, mock_fetch):
def test_store(mock_archive):
spec = Spec('cmake-client').concretized() spec = Spec('cmake-client').concretized()
for s in spec.traverse():
fake_fetchify(mock_archive.url, s.package)
pkg = spec.package pkg = spec.package
try: pkg.do_install()
pkg.do_install()
except Exception:
pkg.remove_prefix()
raise
@pytest.mark.usefixtures('install_mockery') def test_failing_build(install_mockery, mock_fetch):
def test_failing_build(mock_archive):
spec = Spec('failing-build').concretized() spec = Spec('failing-build').concretized()
for s in spec.traverse():
fake_fetchify(mock_archive.url, s.package)
pkg = spec.package pkg = spec.package
with pytest.raises(spack.build_environment.ChildError): with pytest.raises(spack.build_environment.ChildError):
pkg.do_install() pkg.do_install()

View File

@ -26,7 +26,7 @@
class A(AutotoolsPackage): class A(AutotoolsPackage):
"""Simple package with no dependencies""" """Simple package with one optional dependency"""
homepage = "http://www.example.com" homepage = "http://www.example.com"
url = "http://www.example.com/a-1.0.tar.gz" url = "http://www.example.com/a-1.0.tar.gz"

View File

@ -41,4 +41,4 @@ class Libdwarf(Package):
depends_on("libelf") depends_on("libelf")
def install(self, spec, prefix): def install(self, spec, prefix):
pass touch(prefix.libdwarf)

View File

@ -34,11 +34,4 @@ class Libelf(Package):
version('0.8.10', '9db4d36c283d9790d8fa7df1f4d7b4d9') version('0.8.10', '9db4d36c283d9790d8fa7df1f4d7b4d9')
def install(self, spec, prefix): def install(self, spec, prefix):
configure("--prefix=%s" % prefix, touch(prefix.libelf)
"--enable-shared",
"--disable-dependency-tracking",
"--disable-debug")
make()
# The mkdir commands in libelf's intsall can fail in parallel
make("install", parallel=False)