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:
		| @@ -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): | ||||
|   | ||||
| @@ -34,6 +34,7 @@ | ||||
| import inspect | ||||
| import pstats | ||||
| import argparse | ||||
| import tempfile | ||||
|  | ||||
| import llnl.util.tty as tty | ||||
| from llnl.util.tty.color import * | ||||
| @@ -236,10 +237,14 @@ def add_subcommand_group(title, commands): | ||||
|  | ||||
|     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.""" | ||||
|   | ||||
| @@ -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)) | ||||
|   | ||||
| @@ -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 | ||||
|  | ||||
| import llnl.util.filesystem | ||||
| import spack | ||||
| import spack.cmd | ||||
| import spack.cmd.install as install | ||||
|  | ||||
| FILE_REGISTRY = collections.defaultdict(StringIO) | ||||
| from spack.main import SpackCommand | ||||
|  | ||||
|  | ||||
| # 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) | ||||
|  | ||||
|     FILE_REGISTRY[filename] = StringIO() | ||||
|  | ||||
|     try: | ||||
|         yield FILE_REGISTRY[filename] | ||||
|     finally: | ||||
|         handle = FILE_REGISTRY[filename] | ||||
|         FILE_REGISTRY[filename] = handle.getvalue() | ||||
|         handle.close() | ||||
| install = SpackCommand('install') | ||||
|  | ||||
|  | ||||
| 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): | ||||
|         self._dependencies = {} | ||||
|         self.name = name | ||||
|         self.version = version | ||||
|         self.hash = hashStr if hashStr else hash((name, version)) | ||||
|     tmpdir.chdir() | ||||
|     install('--log-format=junit', '--log-file=test.xml', 'libdwarf') | ||||
|  | ||||
|     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 | ||||
|     files = tmpdir.listdir() | ||||
|     filename = tmpdir.join('test.xml') | ||||
|     assert filename in files | ||||
|  | ||||
|     def _find_deps(self, where, deptype): | ||||
|         deptype = self._deptype_norm(deptype) | ||||
|  | ||||
|         return [dep.spec | ||||
|                 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)]) | ||||
|     content = filename.open().read() | ||||
|     assert 'tests="2"' in content | ||||
|     assert 'failures="0"' in content | ||||
|     assert 'errors="0"' in content | ||||
|  | ||||
|  | ||||
| class MockPackage(object): | ||||
| def test_install_package_already_installed( | ||||
|         tmpdir, builtin_mock, mock_archive, mock_fetch, config, | ||||
|         install_mockery): | ||||
|  | ||||
|     def __init__(self, spec, buildLogPath): | ||||
|         self.name = spec.name | ||||
|         self.spec = spec | ||||
|         self.installed = False | ||||
|         self.build_log_path = buildLogPath | ||||
|     tmpdir.chdir() | ||||
|     install('libdwarf') | ||||
|     install('--log-format=junit', '--log-file=test.xml', 'libdwarf') | ||||
|  | ||||
|     def do_install(self, *args, **kwargs): | ||||
|         for x in self.spec.dependencies(): | ||||
|             x.package.do_install(*args, **kwargs) | ||||
|         self.installed = True | ||||
|     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 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 | ||||
|   | ||||
| @@ -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) | ||||
|  | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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 @@ def __init__(self, packages, all=False, force=False, dependents=False): | ||||
|         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 | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -35,16 +35,18 @@ | ||||
|  | ||||
| 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 set_stage(self, stage): | ||||
|             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 | ||||
| ########## | ||||
|   | ||||
| @@ -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 @@ def __getattr__(self, attr): | ||||
|         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() | ||||
|   | ||||
| @@ -26,7 +26,7 @@ | ||||
|  | ||||
|  | ||||
| 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" | ||||
|   | ||||
| @@ -41,4 +41,4 @@ class Libdwarf(Package): | ||||
|     depends_on("libelf") | ||||
|  | ||||
|     def install(self, spec, prefix): | ||||
|         pass | ||||
|         touch(prefix.libdwarf) | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Todd Gamblin
					Todd Gamblin