From f159246d1d073ae7230b5412a4a2a20eac7f49c2 Mon Sep 17 00:00:00 2001 From: Todd Gamblin Date: Sat, 22 Jul 2017 21:27:54 -0700 Subject: 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 --- lib/spack/spack/cmd/__init__.py | 8 +- lib/spack/spack/main.py | 107 +++++++++-- lib/spack/spack/test/cmd/gpg.py | 113 +++++------ lib/spack/spack/test/cmd/install.py | 207 +++------------------ lib/spack/spack/test/cmd/module.py | 5 + lib/spack/spack/test/cmd/python.py | 22 +-- lib/spack/spack/test/cmd/uninstall.py | 33 ++-- lib/spack/spack/test/cmd/url.py | 72 +++---- lib/spack/spack/test/conftest.py | 47 ++++- lib/spack/spack/test/install.py | 70 +------ var/spack/repos/builtin.mock/packages/a/package.py | 2 +- .../builtin.mock/packages/libdwarf/package.py | 2 +- .../repos/builtin.mock/packages/libelf/package.py | 9 +- 13 files changed, 299 insertions(+), 398 deletions(-) diff --git a/lib/spack/spack/cmd/__init__.py b/lib/spack/spack/cmd/__init__.py index f691038734..edeaa677de 100644 --- a/lib/spack/spack/cmd/__init__.py +++ b/lib/spack/spack/cmd/__init__.py @@ -75,7 +75,8 @@ def remove_options(parser, *options): break -def get_cmd_function_name(name): +def get_python_name(name): + """Commands can have '-' in their names, unlike Python identifiers.""" return name.replace("-", "_") @@ -89,7 +90,7 @@ def get_module(name): attr_setdefault(module, SETUP_PARSER, lambda *args: None) # null-op attr_setdefault(module, DESCRIPTION, "") - fn_name = get_cmd_function_name(name) + fn_name = get_python_name(name) if not hasattr(module, fn_name): tty.die("Command module %s (%s) must define function '%s'." % (module.__name__, module.__file__, fn_name)) @@ -99,7 +100,8 @@ def get_module(name): def get_command(name): """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): diff --git a/lib/spack/spack/main.py b/lib/spack/spack/main.py index 3ae8403bcf..2d5648f13e 100644 --- a/lib/spack/spack/main.py +++ b/lib/spack/spack/main.py @@ -34,6 +34,7 @@ import os import inspect import pstats import argparse +import tempfile import llnl.util.tty as tty from llnl.util.tty.color import * @@ -236,10 +237,14 @@ class SpackArgumentParser(argparse.ArgumentParser): def add_command(self, name): """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 if not hasattr(self, 'subparsers'): # 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', dest="command") @@ -322,7 +327,7 @@ def setup_main_options(args): 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 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') +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): + """Run a spack command *and* set spack globaloptions.""" # many operations will fail without a working directory. set_working_dir() @@ -345,12 +432,7 @@ def _main(command, parser, args, unknown_args): # Now actually execute the command try: - 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) + return _invoke_spack_command(command, parser, args, unknown_args) except SpackError as e: e.die() # gracefully die on any SpackErrors except Exception as e: @@ -361,9 +443,6 @@ def _main(command, parser, args, unknown_args): sys.stderr.write('\n') 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): import cProfile @@ -431,7 +510,7 @@ def main(argv=None): # Try to load the particular command the caller asked for. If there # is no module for it, just die. - command_name = args.command[0].replace('-', '_') + command_name = spack.cmd.get_python_name(args.command[0]) try: parser.add_command(command_name) except ImportError: @@ -465,3 +544,7 @@ def main(argv=None): except SystemExit as e: return e.code + + +class SpackCommandError(Exception): + """Raised when SpackCommand execution fails.""" diff --git a/lib/spack/spack/test/cmd/gpg.py b/lib/spack/spack/test/cmd/gpg.py index 189c827d05..97f7accd60 100644 --- a/lib/spack/spack/test/cmd/gpg.py +++ b/lib/spack/spack/test/cmd/gpg.py @@ -22,13 +22,12 @@ # License along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA ############################################################################## -import argparse import os import pytest import spack -import spack.cmd.gpg as gpg import spack.util.gpg as gpg_util +from spack.main import SpackCommand from spack.util.executable import ProcessError @@ -40,6 +39,19 @@ def testing_gpg_directory(tmpdir): 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(): try: gpg_util.Gpg.gpg()('--version', output=os.devnull) @@ -48,45 +60,31 @@ def has_gnupg2(): return False -@pytest.mark.usefixtures('testing_gpg_directory') +@pytest.mark.xfail # TODO: fix failing tests. @pytest.mark.skipif(not has_gnupg2(), reason='These tests require gnupg2') -def test_gpg(tmpdir): - parser = argparse.ArgumentParser() - gpg.setup_parser(parser) - +def test_gpg(gpg, tmpdir, testing_gpg_directory, mock_gpg_config): # 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): - gpg.gpg(parser, args) + gpg('verify', os.path.join(spack.mock_gpg_data_path, 'content.txt')) # Import the default key. - args = parser.parse_args(['init']) - args.import_dir = spack.mock_gpg_keys_path - gpg.gpg(parser, args) + gpg('init') # List the keys. # TODO: Test the output here. - args = parser.parse_args(['list', '--trusted']) - gpg.gpg(parser, args) - args = parser.parse_args(['list', '--signing']) - gpg.gpg(parser, args) + gpg('list', '--trusted') + gpg('list', '--signing') # Verify the file now that the key has been trusted. - args = parser.parse_args(['verify', os.path.join( - spack.mock_gpg_data_path, 'content.txt')]) - gpg.gpg(parser, args) + gpg('verify', os.path.join(spack.mock_gpg_data_path, 'content.txt')) # Untrust the default key. - args = parser.parse_args(['untrust', 'Spack testing']) - gpg.gpg(parser, args) + gpg('untrust', 'Spack testing') # 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): - gpg.gpg(parser, args) + gpg('verify', os.path.join(spack.mock_gpg_data_path, 'content.txt')) # Create a file to test signing. test_path = tmpdir.join('to-sign.txt') @@ -94,88 +92,71 @@ def test_gpg(tmpdir): fout.write('Test content for signing.\n') # Signing without a private key should fail. - args = parser.parse_args(['sign', str(test_path)]) 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' # Create a key for use in the tests. keypath = tmpdir.join('testing-1.key') - args = parser.parse_args(['create', - '--comment', 'Spack testing key', - '--export', str(keypath), - 'Spack testing 1', - 'spack@googlegroups.com']) - gpg.gpg(parser, args) + gpg('create', + '--comment', 'Spack testing key', + '--export', str(keypath), + 'Spack testing 1', + 'spack@googlegroups.com') keyfp = gpg_util.Gpg.signing_keys()[0] # List the keys. # TODO: Test the output here. - args = parser.parse_args(['list', '--trusted']) - gpg.gpg(parser, args) - args = parser.parse_args(['list', '--signing']) - gpg.gpg(parser, args) + gpg('list', '--trusted') + gpg('list', '--signing') # Signing with the default (only) key. - args = parser.parse_args(['sign', str(test_path)]) - gpg.gpg(parser, args) + gpg('sign', str(test_path)) # Verify the file we just verified. - args = parser.parse_args(['verify', str(test_path)]) - gpg.gpg(parser, args) + gpg('verify', str(test_path)) # Export the key for future use. export_path = tmpdir.join('export.testing.key') - args = parser.parse_args(['export', str(export_path)]) - gpg.gpg(parser, args) + gpg('export', str(export_path)) # Create a second key for use in the tests. - args = parser.parse_args(['create', - '--comment', 'Spack testing key', - 'Spack testing 2', - 'spack@googlegroups.com']) - gpg.gpg(parser, args) + gpg('create', + '--comment', 'Spack testing key', + 'Spack testing 2', + 'spack@googlegroups.com') # List the keys. # TODO: Test the output here. - args = parser.parse_args(['list', '--trusted']) - gpg.gpg(parser, args) - args = parser.parse_args(['list', '--signing']) - gpg.gpg(parser, args) + gpg('list', '--trusted') + gpg('list', '--signing') test_path = tmpdir.join('to-sign-2.txt') with open(str(test_path), 'w+') as fout: fout.write('Test content for signing.\n') # Signing with multiple signing keys is ambiguous. - args = parser.parse_args(['sign', str(test_path)]) with pytest.raises(RuntimeError) as exc_info: - gpg.gpg(parser, args) + gpg('sign', str(test_path)) assert exc_info.value.args[0] == \ 'multiple signing keys are available; please choose one' # Signing with a specified key. - args = parser.parse_args(['sign', '--key', keyfp, str(test_path)]) - gpg.gpg(parser, args) + gpg('sign', '--key', keyfp, str(test_path)) # Untrusting signing keys needs a flag. - args = parser.parse_args(['untrust', 'Spack testing 1']) with pytest.raises(ProcessError): - gpg.gpg(parser, args) + gpg('untrust', 'Spack testing 1') # Untrust the key we created. - args = parser.parse_args(['untrust', '--signing', keyfp]) - gpg.gpg(parser, args) + gpg('untrust', '--signing', keyfp) # Verification should now fail. - args = parser.parse_args(['verify', str(test_path)]) with pytest.raises(ProcessError): - gpg.gpg(parser, args) + gpg('verify', str(test_path)) # Trust the exported key. - args = parser.parse_args(['trust', str(export_path)]) - gpg.gpg(parser, args) + gpg('trust', str(export_path)) # Verification should now succeed again. - args = parser.parse_args(['verify', str(test_path)]) - gpg.gpg(parser, args) + gpg('verify', str(test_path)) diff --git a/lib/spack/spack/test/cmd/install.py b/lib/spack/spack/test/cmd/install.py index 7f9db1baa2..951d9d85d7 100644 --- a/lib/spack/spack/test/cmd/install.py +++ b/lib/spack/spack/test/cmd/install.py @@ -22,194 +22,45 @@ # License along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA ############################################################################## -import argparse -import codecs -import collections -import contextlib -import unittest -from six import StringIO +from spack.main import SpackCommand -import llnl.util.filesystem -import spack -import spack.cmd -import spack.cmd.install as install -FILE_REGISTRY = collections.defaultdict(StringIO) +install = SpackCommand('install') -# Monkey-patch open to write module files to a StringIO instance -@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) +def _install_package_and_dependency( + tmpdir, builtin_mock, mock_archive, mock_fetch, config, + install_mockery): - FILE_REGISTRY[filename] = StringIO() + tmpdir.chdir() + install('--log-format=junit', '--log-file=test.xml', 'libdwarf') - try: - yield FILE_REGISTRY[filename] - finally: - handle = FILE_REGISTRY[filename] - FILE_REGISTRY[filename] = handle.getvalue() - handle.close() + files = tmpdir.listdir() + filename = tmpdir.join('test.xml') + assert filename in files + content = filename.open().read() + assert 'tests="2"' in content + assert 'failures="0"' in content + assert 'errors="0"' in content -class MockSpec(object): - def __init__(self, name, version, hashStr=None): - self._dependencies = {} - self.name = name - self.version = version - self.hash = hashStr if hashStr else hash((name, version)) +def test_install_package_already_installed( + tmpdir, builtin_mock, mock_archive, mock_fetch, config, + install_mockery): - def _deptype_norm(self, deptype): - if deptype is None: - return spack.alldeps - # Force deptype to be a tuple so that we can do set intersections. - if isinstance(deptype, str): - return (deptype,) - return deptype + tmpdir.chdir() + install('libdwarf') + install('--log-format=junit', '--log-file=test.xml', 'libdwarf') - def _find_deps(self, where, deptype): - deptype = self._deptype_norm(deptype) + files = tmpdir.listdir() + filename = tmpdir.join('test.xml') + assert filename in files - return [dep.spec - for dep in where.values() - if deptype and any(d in deptype for d in dep.deptypes)] + content = filename.open().read() + assert 'tests="2"' in content + assert 'failures="0"' in content + assert 'errors="0"' in content - 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 __init__(self, spec, buildLogPath): - self.name = spec.name - self.spec = spec - self.installed = False - self.build_log_path = buildLogPath - - def do_install(self, *args, **kwargs): - for x in self.spec.dependencies(): - x.package.do_install(*args, **kwargs) - self.installed = True - - -class MockPackageDb(object): - - 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) + skipped = [line for line in content.split('\n') if 'skipped' in line] + assert len(skipped) == 2 diff --git a/lib/spack/spack/test/cmd/module.py b/lib/spack/spack/test/cmd/module.py index 8d15afdd0c..b8f24856be 100644 --- a/lib/spack/spack/test/cmd/module.py +++ b/lib/spack/spack/test/cmd/module.py @@ -70,15 +70,20 @@ def test_remove_and_add_tcl(database, parser): # Remove existing modules [tcl] args = parser.parse_args(['rm', '-y', 'mpileaks']) module_files = _get_module_files(args) + for item in module_files: assert os.path.exists(item) + module.module(parser, args) + for item in module_files: assert not os.path.exists(item) # Add them back [tcl] args = parser.parse_args(['refresh', '-y', 'mpileaks']) + module.module(parser, args) + for item in module_files: assert os.path.exists(item) diff --git a/lib/spack/spack/test/cmd/python.py b/lib/spack/spack/test/cmd/python.py index 5ad8456e49..5e3ea83053 100644 --- a/lib/spack/spack/test/cmd/python.py +++ b/lib/spack/spack/test/cmd/python.py @@ -22,22 +22,12 @@ # License along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA ############################################################################## -import argparse -import pytest +import spack +from spack.main import SpackCommand -from spack.cmd.python import * +python = SpackCommand('python') -@pytest.fixture(scope='module') -def parser(): - """Returns the parser for the ``python`` command""" - 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) +def test_python(): + out, err = python('-c', 'import spack; print(spack.spack_version)') + assert out.strip() == str(spack.spack_version) diff --git a/lib/spack/spack/test/cmd/uninstall.py b/lib/spack/spack/test/cmd/uninstall.py index a47c76715b..72dda23f93 100644 --- a/lib/spack/spack/test/cmd/uninstall.py +++ b/lib/spack/spack/test/cmd/uninstall.py @@ -24,7 +24,9 @@ ############################################################################## import pytest import spack.store -import spack.cmd.uninstall +from spack.main import SpackCommand, SpackCommandError + +uninstall = SpackCommand('uninstall') class MockArgs(object): @@ -37,20 +39,21 @@ class MockArgs(object): self.yes_to_all = True -def test_uninstall(database): - parser = None - uninstall = spack.cmd.uninstall.uninstall - # Multiple matches - args = MockArgs(['mpileaks']) - with pytest.raises(SystemExit): - uninstall(parser, args) - # Installed dependents - args = MockArgs(['libelf']) - with pytest.raises(SystemExit): - uninstall(parser, args) - # Recursive uninstall - args = MockArgs(['callpath'], all=True, dependents=True) - uninstall(parser, args) +def test_multiple_matches(database): + """Test unable to uninstall when multiple matches.""" + with pytest.raises(SpackCommandError): + uninstall('-y', 'mpileaks') + + +def test_installed_dependents(database): + """Test can't uninstall when ther are installed dependents.""" + with pytest.raises(SpackCommandError): + uninstall('-y', 'libelf') + + +def test_recursive_uninstall(database): + """Test recursive uninstall.""" + uninstall('-y', '-a', '--dependents', 'callpath') all_specs = spack.store.layout.all_specs() assert len(all_specs) == 8 diff --git a/lib/spack/spack/test/cmd/url.py b/lib/spack/spack/test/cmd/url.py index ae585b328f..21f88e928b 100644 --- a/lib/spack/spack/test/cmd/url.py +++ b/lib/spack/spack/test/cmd/url.py @@ -22,18 +22,13 @@ # License along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA ############################################################################## -import argparse +import re import pytest - +from spack.url import UndetectableVersionError +from spack.main import SpackCommand from spack.cmd.url import * - -@pytest.fixture(scope='module') -def parser(): - """Returns the parser for the ``url`` command""" - parser = argparse.ArgumentParser() - setup_parser(parser) - return parser +url = SpackCommand('url') 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 -def test_url_parse(parser): - args = parser.parse_args(['parse', 'http://zlib.net/fossils/zlib-1.2.10.tar.gz']) - url(parser, args) +def test_url_parse(): + url('parse', 'http://zlib.net/fossils/zlib-1.2.10.tar.gz') -@pytest.mark.xfail -def test_url_parse_xfail(parser): +def test_url_with_no_version_fails(): # No version in URL - args = parser.parse_args(['parse', 'http://www.netlib.org/voronoi/triangle.zip']) - url(parser, args) + with pytest.raises(UndetectableVersionError): + url('parse', 'http://www.netlib.org/voronoi/triangle.zip') -def test_url_list(parser): - args = parser.parse_args(['list']) - total_urls = url_list(args) +def test_url_list(): + out, err = url('list') + total_urls = len(out.split('\n')) # The following two options should not change the number of URLs printed. - args = parser.parse_args(['list', '--color', '--extrapolation']) - colored_urls = url_list(args) + out, err = url('list', '--color', '--extrapolation') + colored_urls = len(out.split('\n')) assert colored_urls == total_urls # The following options should print fewer URLs than the default. # If they print the same number of URLs, something is horribly broken. # If they say we missed 0 URLs, something is probably broken too. - args = parser.parse_args(['list', '--incorrect-name']) - incorrect_name_urls = url_list(args) + out, err = url('list', '--incorrect-name') + incorrect_name_urls = len(out.split('\n')) assert 0 < incorrect_name_urls < total_urls - args = parser.parse_args(['list', '--incorrect-version']) - incorrect_version_urls = url_list(args) + out, err = url('list', '--incorrect-version') + incorrect_version_urls = len(out.split('\n')) assert 0 < incorrect_version_urls < total_urls - args = parser.parse_args(['list', '--correct-name']) - correct_name_urls = url_list(args) + out, err = url('list', '--correct-name') + correct_name_urls = len(out.split('\n')) assert 0 < correct_name_urls < total_urls - args = parser.parse_args(['list', '--correct-version']) - correct_version_urls = url_list(args) + out, err = url('list', '--correct-version') + correct_version_urls = len(out.split('\n')) assert 0 < correct_version_urls < total_urls -def test_url_summary(parser): - args = parser.parse_args(['summary']) +def test_url_summary(): + """Test the URL summary command.""" + # test url_summary, the internal function that does the work (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_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 diff --git a/lib/spack/spack/test/conftest.py b/lib/spack/spack/test/conftest.py index c23fb466a5..e0a745c7e9 100644 --- a/lib/spack/spack/test/conftest.py +++ b/lib/spack/spack/test/conftest.py @@ -35,16 +35,18 @@ import ordereddict_backport import py import pytest + import spack import spack.architecture import spack.database import spack.directory_layout -import spack.fetch_strategy import spack.platforms.test import spack.repository import spack.stage import spack.util.executable import spack.util.pattern +from spack.package import PackageBase +from spack.fetch_strategy import * ########## @@ -78,12 +80,10 @@ def mock_fetch_cache(monkeypatch): pass def fetch(self): - raise spack.fetch_strategy.FetchError( - 'Mock cache always fails for tests' - ) + raise FetchError('Mock cache always fails for tests') def __str__(self): - return "[mock fetcher]" + return "[mock fetch cache]" monkeypatch.setattr(spack, 'fetch_cache', MockCache()) @@ -287,6 +287,43 @@ def refresh_db_on_exit(database): yield 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 ########## diff --git a/lib/spack/spack/test/install.py b/lib/spack/spack/test/install.py index 3a934d5ea2..278b2efb70 100644 --- a/lib/spack/spack/test/install.py +++ b/lib/spack/spack/test/install.py @@ -22,45 +22,15 @@ # License along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA ############################################################################## +import os import pytest + import spack 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 -import os - -@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): +def test_install_and_uninstall(install_mockery, mock_fetch): # Get a basic concrete spec for the trivial install package. spec = Spec('trivial-install-test-package') spec.concretize() @@ -69,8 +39,6 @@ def test_install_and_uninstall(mock_archive): # Get the package pkg = spack.repo.get(spec) - fake_fetchify(mock_archive.url, pkg) - try: pkg.do_install() pkg.do_uninstall() @@ -114,12 +82,10 @@ class MockStage(object): return getattr(self.wrapped_stage, attr) -@pytest.mark.usefixtures('install_mockery') -def test_partial_install_delete_prefix_and_stage(mock_archive): +def test_partial_install_delete_prefix_and_stage(install_mockery, mock_fetch): spec = Spec('canfail') spec.concretize() pkg = spack.repo.get(spec) - fake_fetchify(mock_archive.url, pkg) remove_prefix = spack.package.Package.remove_prefix instance_rm_prefix = pkg.remove_prefix @@ -145,14 +111,12 @@ def test_partial_install_delete_prefix_and_stage(mock_archive): pass -@pytest.mark.usefixtures('install_mockery') -def test_partial_install_keep_prefix(mock_archive): +def test_partial_install_keep_prefix(install_mockery, mock_fetch): spec = Spec('canfail') spec.concretize() pkg = spack.repo.get(spec) # Normally the stage should start unset, but other tests set it pkg._stage = None - fake_fetchify(mock_archive.url, pkg) remove_prefix = spack.package.Package.remove_prefix try: # 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 -@pytest.mark.usefixtures('install_mockery') -def test_second_install_no_overwrite_first(mock_archive): +def test_second_install_no_overwrite_first(install_mockery, mock_fetch): spec = Spec('canfail') spec.concretize() pkg = spack.repo.get(spec) - fake_fetchify(mock_archive.url, pkg) remove_prefix = spack.package.Package.remove_prefix try: spack.package.Package.remove_prefix = mock_remove_prefix @@ -198,28 +160,14 @@ def test_second_install_no_overwrite_first(mock_archive): pass -@pytest.mark.usefixtures('install_mockery') -def test_store(mock_archive): +def test_store(install_mockery, mock_fetch): spec = Spec('cmake-client').concretized() - - for s in spec.traverse(): - fake_fetchify(mock_archive.url, s.package) - pkg = spec.package - try: - pkg.do_install() - except Exception: - pkg.remove_prefix() - raise + pkg.do_install() -@pytest.mark.usefixtures('install_mockery') -def test_failing_build(mock_archive): +def test_failing_build(install_mockery, mock_fetch): spec = Spec('failing-build').concretized() - - for s in spec.traverse(): - fake_fetchify(mock_archive.url, s.package) - pkg = spec.package with pytest.raises(spack.build_environment.ChildError): pkg.do_install() diff --git a/var/spack/repos/builtin.mock/packages/a/package.py b/var/spack/repos/builtin.mock/packages/a/package.py index 59d8b9e330..e2fa6c35b0 100644 --- a/var/spack/repos/builtin.mock/packages/a/package.py +++ b/var/spack/repos/builtin.mock/packages/a/package.py @@ -26,7 +26,7 @@ from spack import * class A(AutotoolsPackage): - """Simple package with no dependencies""" + """Simple package with one optional dependency""" homepage = "http://www.example.com" url = "http://www.example.com/a-1.0.tar.gz" diff --git a/var/spack/repos/builtin.mock/packages/libdwarf/package.py b/var/spack/repos/builtin.mock/packages/libdwarf/package.py index 10d6fa8e88..0cdbaf2a33 100644 --- a/var/spack/repos/builtin.mock/packages/libdwarf/package.py +++ b/var/spack/repos/builtin.mock/packages/libdwarf/package.py @@ -41,4 +41,4 @@ class Libdwarf(Package): depends_on("libelf") def install(self, spec, prefix): - pass + touch(prefix.libdwarf) diff --git a/var/spack/repos/builtin.mock/packages/libelf/package.py b/var/spack/repos/builtin.mock/packages/libelf/package.py index 3a2fe603ef..0390963081 100644 --- a/var/spack/repos/builtin.mock/packages/libelf/package.py +++ b/var/spack/repos/builtin.mock/packages/libelf/package.py @@ -34,11 +34,4 @@ class Libelf(Package): version('0.8.10', '9db4d36c283d9790d8fa7df1f4d7b4d9') def install(self, spec, prefix): - configure("--prefix=%s" % prefix, - "--enable-shared", - "--disable-dependency-tracking", - "--disable-debug") - make() - - # The mkdir commands in libelf's intsall can fail in parallel - make("install", parallel=False) + touch(prefix.libelf) -- cgit v1.2.3-60-g2f50