From d1a5857a03dc2e21f5514fa2bcfafe0ec0210f12 Mon Sep 17 00:00:00 2001 From: Massimiliano Culpo Date: Wed, 6 Sep 2017 03:44:42 +0200 Subject: Added support for querying by tags (#4786) * Added support to query packages by tags. - The querying commands `spack list`, `spack find` and `spack info` have been modified to support querying by tags. Tests have been added to check that the feature is working correctly under what should be the most frequent use cases. * Refactored Repo class to make insertion of new file caches easier. - Added the class FastPackageChecker. This class is a Mapping from package names to stat info, that gets memoized for faster access. - Extracted the creation of a ProviderIndex to its own factory function. * Added a cache file for tags. - Following what was done for providers, a TagIndex class has been added. This class can serialize and deserialize objects from json. Repo and RepoPath have a new method 'packages_with_tags', that uses the TagIndex to compute a list of package names that have all the tags passed as arguments. On Ubuntu 14.04 the effect if the cache reduces the time for spack list from ~3sec. to ~0.3sec. after the cache has been built. * Fixed colorization of `spack info` --- lib/spack/spack/cmd/common/arguments.py | 14 +- lib/spack/spack/cmd/find.py | 13 +- lib/spack/spack/cmd/info.py | 26 ++- lib/spack/spack/cmd/list.py | 14 +- lib/spack/spack/file_cache.py | 2 +- lib/spack/spack/repository.py | 394 +++++++++++++++++++++++--------- lib/spack/spack/test/cmd/find.py | 57 +++++ lib/spack/spack/test/cmd/info.py | 52 +++++ lib/spack/spack/test/cmd/list.py | 73 ++++++ 9 files changed, 518 insertions(+), 127 deletions(-) create mode 100644 lib/spack/spack/test/cmd/list.py (limited to 'lib') diff --git a/lib/spack/spack/cmd/common/arguments.py b/lib/spack/spack/cmd/common/arguments.py index 11c90b2ee4..46be346837 100644 --- a/lib/spack/spack/cmd/common/arguments.py +++ b/lib/spack/spack/cmd/common/arguments.py @@ -26,15 +26,23 @@ import argparse import spack.cmd -import spack.store import spack.modules +import spack.spec +import spack.store from spack.util.pattern import Args + __all__ = ['add_common_arguments'] _arguments = {} def add_common_arguments(parser, list_of_arguments): + """Extend a parser with extra arguments + + Args: + parser: parser to be extended + list_of_arguments: arguments to be added to the parser + """ for argument in list_of_arguments: if argument not in _arguments: message = 'Trying to add non existing argument "{0}" to a command' @@ -133,3 +141,7 @@ _arguments['long'] = Args( _arguments['very_long'] = Args( '-L', '--very-long', action='store_true', help='show full dependency hashes as well as versions') + +_arguments['tags'] = Args( + '-t', '--tags', action='append', + help='filter a package query by tags') diff --git a/lib/spack/spack/cmd/find.py b/lib/spack/spack/cmd/find.py index 02ff10f425..b7aa390c59 100644 --- a/lib/spack/spack/cmd/find.py +++ b/lib/spack/spack/cmd/find.py @@ -25,8 +25,8 @@ import sys import llnl.util.tty as tty +import spack import spack.cmd.common.arguments as arguments - from spack.cmd import display_specs description = "list and search installed packages" @@ -54,7 +54,7 @@ def setup_parser(subparser): const='deps', help='show full dependency DAG of installed packages') - arguments.add_common_arguments(subparser, ['long', 'very_long']) + arguments.add_common_arguments(subparser, ['long', 'very_long', 'tags']) subparser.add_argument('-f', '--show-flags', action='store_true', @@ -123,11 +123,16 @@ def find(parser, args): # Exit early if no package matches the constraint if not query_specs and args.constraint: - msg = "No package matches the query: {0}".format( - ' '.join(args.constraint)) + msg = "No package matches the query: {0}" + msg = msg.format(' '.join(args.constraint)) tty.msg(msg) return + # If tags have been specified on the command line, filter by tags + if args.tags: + packages_with_tags = spack.repo.packages_with_tags(*args.tags) + query_specs = [x for x in query_specs if x.name in packages_with_tags] + # Display the result if sys.stdout.isatty(): tty.msg("%d installed packages." % len(query_specs)) diff --git a/lib/spack/spack/cmd/info.py b/lib/spack/spack/cmd/info.py index b7f824c091..3236998710 100644 --- a/lib/spack/spack/cmd/info.py +++ b/lib/spack/spack/cmd/info.py @@ -26,15 +26,15 @@ from __future__ import print_function import textwrap +from six.moves import zip_longest + +from llnl.util.tty.colify import * + import llnl.util.tty.color as color import spack import spack.fetch_strategy as fs import spack.spec -from llnl.util.tty.colify import * - -from six.moves import zip_longest - description = 'get detailed information on a particular package' section = 'basic' level = 'short' @@ -156,9 +156,9 @@ def print_text_info(pkg): color.cprint('') color.cprint(section_title('Description:')) if pkg.__doc__: - print(pkg.format_doc(indent=4)) + color.cprint(pkg.format_doc(indent=4)) else: - print(" None") + color.cprint(" None") color.cprint(section_title('Homepage: ') + pkg.homepage) @@ -167,6 +167,14 @@ def print_text_info(pkg): color.cprint('') color.cprint(section_title('Maintainers: ') + mnt) + color.cprint('') + color.cprint(section_title("Tags: ")) + if hasattr(pkg, 'tags'): + tags = sorted(pkg.tags) + colify(tags, indent=4) + else: + color.cprint(" None") + color.cprint('') color.cprint(section_title('Preferred version: ')) @@ -220,7 +228,7 @@ def print_text_info(pkg): if deps: colify(deps, indent=4) else: - print(' None') + color.cprint(' None') color.cprint('') color.cprint(section_title('Virtual Packages: ')) @@ -238,7 +246,9 @@ def print_text_info(pkg): print(line) else: - print(" None") + color.cprint(" None") + + color.cprint('') def info(parser, args): diff --git a/lib/spack/spack/cmd/list.py b/lib/spack/spack/cmd/list.py index 5b915b94ec..730ad8d9d9 100644 --- a/lib/spack/spack/cmd/list.py +++ b/lib/spack/spack/cmd/list.py @@ -29,12 +29,15 @@ import cgi import fnmatch import re import sys + from six import StringIO import llnl.util.tty as tty -import spack from llnl.util.tty.colify import colify +import spack +import spack.cmd.common.arguments as arguments + description = "list and search available packages" section = "basic" level = "short" @@ -60,6 +63,8 @@ def setup_parser(subparser): '--format', default='name_only', choices=formatters, help='format to be used to print the output [default: name_only]') + arguments.add_common_arguments(subparser, ['tags']) + def filter_by_name(pkgs, args): """ @@ -183,5 +188,12 @@ def list(parser, args): pkgs = set(spack.repo.all_package_names()) # Filter the set appropriately sorted_packages = filter_by_name(pkgs, args) + + # Filter by tags + if args.tags: + packages_with_tags = set(spack.repo.packages_with_tags(*args.tags)) + sorted_packages = set(sorted_packages) & packages_with_tags + sorted_packages = sorted(sorted_packages) + # Print to stdout formatters[args.format](sorted_packages) diff --git a/lib/spack/spack/file_cache.py b/lib/spack/spack/file_cache.py index f09be4ecd5..6a6d59942e 100644 --- a/lib/spack/spack/file_cache.py +++ b/lib/spack/spack/file_cache.py @@ -35,7 +35,7 @@ class FileCache(object): """This class manages cached data in the filesystem. - Cache files are fetched and stored by unique keys. Keys can be relative - paths, so that thre can be some hierarchy in the cache. + paths, so that there can be some hierarchy in the cache. - The FileCache handles locking cache files for reading and writing, so client code need not manage locks for cache entries. diff --git a/lib/spack/spack/repository.py b/lib/spack/spack/repository.py index 0a6a774315..fbca9474fb 100644 --- a/lib/spack/spack/repository.py +++ b/lib/spack/spack/repository.py @@ -22,6 +22,7 @@ # 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 collections import os import stat import shutil @@ -31,11 +32,18 @@ import inspect import imp import re import traceback -from bisect import bisect_left +import json + +try: + from collections.abc import Mapping +except ImportError: + from collections import Mapping + from types import ModuleType import yaml +import llnl.util.lang import llnl.util.tty as tty from llnl.util.filesystem import * @@ -93,6 +101,242 @@ class SpackNamespace(ModuleType): return getattr(self, name) +class FastPackageChecker(Mapping): + """Cache that maps package names to the stats obtained on the + 'package.py' files associated with them. + + For each repository a cache is maintained at class level, and shared among + all instances referring to it. Update of the global cache is done lazily + during instance initialization. + """ + #: Global cache, reused by every instance + _paths_cache = {} + + def __init__(self, packages_path): + + #: The path of the repository managed by this instance + self.packages_path = packages_path + + # If the cache we need is not there yet, then build it appropriately + if packages_path not in self._paths_cache: + self._paths_cache[packages_path] = self._create_new_cache() + + #: Reference to the appropriate entry in the global cache + self._packages_to_stats = self._paths_cache[packages_path] + + def _create_new_cache(self): + """Create a new cache for packages in a repo. + + The implementation here should try to minimize filesystem + calls. At the moment, it is O(number of packages) and makes + about one stat call per package. This is reasonably fast, and + avoids actually importing packages in Spack, which is slow. + """ + # Create a dictionary that will store the mapping between a + # package name and its stat info + cache = {} + for pkg_name in os.listdir(self.packages_path): + # Skip non-directories in the package root. + pkg_dir = join_path(self.packages_path, pkg_name) + + # Warn about invalid names that look like packages. + if not valid_module_name(pkg_name): + msg = 'Skipping package at {0}. ' + msg += '"{1]" is not a valid Spack module name.' + tty.warn(msg.format(pkg_dir, pkg_name)) + continue + + # Construct the file name from the directory + pkg_file = os.path.join( + self.packages_path, pkg_name, package_file_name + ) + + # Use stat here to avoid lots of calls to the filesystem. + try: + sinfo = os.stat(pkg_file) + except OSError as e: + if e.errno == errno.ENOENT: + # No package.py file here. + continue + elif e.errno == errno.EACCES: + tty.warn("Can't read package file %s." % pkg_file) + continue + raise e + + # If it's not a file, skip it. + if stat.S_ISDIR(sinfo.st_mode): + continue + + # If it is a file, then save the stats under the + # appropriate key + cache[pkg_name] = sinfo + + return cache + + def __getitem__(self, item): + return self._packages_to_stats[item] + + def __iter__(self): + return iter(self._packages_to_stats) + + def __len__(self): + return len(self._packages_to_stats) + + +class TagIndex(Mapping): + """Maps tags to list of packages.""" + + def __init__(self): + self._tag_dict = collections.defaultdict(list) + + def to_json(self, stream): + json.dump({'tags': self._tag_dict}, stream) + + @staticmethod + def from_json(stream): + d = json.load(stream) + + r = TagIndex() + + for tag, list in d['tags'].items(): + r[tag].extend(list) + + return r + + def __getitem__(self, item): + return self._tag_dict[item] + + def __iter__(self): + return iter(self._tag_dict) + + def __len__(self): + return len(self._tag_dict) + + def update_package(self, pkg_name): + """Updates a package in the tag index. + + Args: + pkg_name (str): name of the package to be removed from the index + + """ + + package = spack.repo.get(pkg_name) + + # Remove the package from the list of packages, if present + for pkg_list in self._tag_dict.values(): + if pkg_name in pkg_list: + pkg_list.remove(pkg_name) + + # Add it again under the appropriate tags + for tag in getattr(package, 'tags', []): + self._tag_dict[tag].append(package.name) + + +@llnl.util.lang.memoized +def make_provider_index_cache(packages_path, namespace): + """Lazily updates the provider index cache associated with a repository, + if need be, then returns it. Caches results for later look-ups. + + Args: + packages_path: path of the repository + namespace: namespace of the repository + + Returns: + instance of ProviderIndex + """ + # Map that goes from package names to stat info + fast_package_checker = FastPackageChecker(packages_path) + + # Filename of the provider index cache + cache_filename = 'providers/{0}-index.yaml'.format(namespace) + + # Compute which packages needs to be updated in the cache + index_mtime = spack.misc_cache.mtime(cache_filename) + + needs_update = [ + x for x, sinfo in fast_package_checker.items() + if sinfo.st_mtime > index_mtime + ] + + # Read the old ProviderIndex, or make a new one. + index_existed = spack.misc_cache.init_entry(cache_filename) + + if index_existed and not needs_update: + + # If the provider index exists and doesn't need an update + # just read from it + with spack.misc_cache.read_transaction(cache_filename) as f: + index = ProviderIndex.from_yaml(f) + + else: + + # Otherwise we need a write transaction to update it + with spack.misc_cache.write_transaction(cache_filename) as (old, new): + + index = ProviderIndex.from_yaml(old) if old else ProviderIndex() + + for pkg_name in needs_update: + namespaced_name = '{0}.{1}'.format(namespace, pkg_name) + index.remove_provider(namespaced_name) + index.update(namespaced_name) + + index.to_yaml(new) + + return index + + +@llnl.util.lang.memoized +def make_tag_index_cache(packages_path, namespace): + """Lazily updates the tag index cache associated with a repository, + if need be, then returns it. Caches results for later look-ups. + + Args: + packages_path: path of the repository + namespace: namespace of the repository + + Returns: + instance of TagIndex + """ + # Map that goes from package names to stat info + fast_package_checker = FastPackageChecker(packages_path) + + # Filename of the provider index cache + cache_filename = 'tags/{0}-index.json'.format(namespace) + + # Compute which packages needs to be updated in the cache + index_mtime = spack.misc_cache.mtime(cache_filename) + + needs_update = [ + x for x, sinfo in fast_package_checker.items() + if sinfo.st_mtime > index_mtime + ] + + # Read the old ProviderIndex, or make a new one. + index_existed = spack.misc_cache.init_entry(cache_filename) + + if index_existed and not needs_update: + + # If the provider index exists and doesn't need an update + # just read from it + with spack.misc_cache.read_transaction(cache_filename) as f: + index = TagIndex.from_json(f) + + else: + + # Otherwise we need a write transaction to update it + with spack.misc_cache.write_transaction(cache_filename) as (old, new): + + index = TagIndex.from_json(old) if old else TagIndex() + + for pkg_name in needs_update: + namespaced_name = '{0}.{1}'.format(namespace, pkg_name) + index.update_package(namespaced_name) + + index.to_json(new) + + return index + + class RepoPath(object): """A RepoPath is a list of repos that function as one. @@ -220,6 +464,12 @@ class RepoPath(object): self._all_package_names = sorted(all_pkgs, key=lambda n: n.lower()) return self._all_package_names + def packages_with_tags(self, *tags): + r = set() + for repo in self.repos: + r |= set(repo.packages_with_tags(*tags)) + return sorted(r) + def all_packages(self): for name in self.all_package_names(): yield self.get(name) @@ -422,21 +672,18 @@ class Repo(object): self._classes = {} self._instances = {} - # list of packages that are newer than the index. - self._needs_update = [] + # Maps that goes from package name to corresponding file stat + self._fast_package_checker = FastPackageChecker(self.packages_path) - # Index of virtual dependencies + # Index of virtual dependencies, computed lazily self._provider_index = None - # Cached list of package names. - self._all_package_names = None + # Index of tags, computed lazily + self._tag_index = None # make sure the namespace for packages in this repo exists. self._create_namespace() - # Unique filename for cache of virtual dependency providers - self._cache_file = 'providers/%s-index.yaml' % self.namespace - def _create_namespace(self): """Create this repo's namespace module and insert it into sys.modules. @@ -617,41 +864,28 @@ class Repo(object): """Clear entire package instance cache.""" self._instances.clear() - def _update_provider_index(self): - # Check modification dates of all packages - self._fast_package_check() - - def read(): - with open(self.index_file) as f: - self._provider_index = ProviderIndex.from_yaml(f) - - # Read the old ProviderIndex, or make a new one. - key = self._cache_file - index_existed = spack.misc_cache.init_entry(key) - if index_existed and not self._needs_update: - with spack.misc_cache.read_transaction(key) as f: - self._provider_index = ProviderIndex.from_yaml(f) - else: - with spack.misc_cache.write_transaction(key) as (old, new): - if old: - self._provider_index = ProviderIndex.from_yaml(old) - else: - self._provider_index = ProviderIndex() - - for pkg_name in self._needs_update: - namespaced_name = '%s.%s' % (self.namespace, pkg_name) - self._provider_index.remove_provider(namespaced_name) - self._provider_index.update(namespaced_name) - - self._provider_index.to_yaml(new) - @property def provider_index(self): """A provider index with names *specific* to this repo.""" + if self._provider_index is None: - self._update_provider_index() + self._provider_index = make_provider_index_cache( + self.packages_path, self.namespace + ) + return self._provider_index + @property + def tag_index(self): + """A provider index with names *specific* to this repo.""" + + if self._tag_index is None: + self._tag_index = make_tag_index_cache( + self.packages_path, self.namespace + ) + + return self._tag_index + @_autospec def providers_for(self, vpkg_spec): providers = self.provider_index.providers_for(vpkg_spec) @@ -689,73 +923,18 @@ class Repo(object): pkg_dir = self.dirname_for_package_name(spec.name) return join_path(pkg_dir, package_file_name) - def _fast_package_check(self): - """List packages in the repo and check whether index is up to date. - - Both of these opreations require checking all `package.py` - files so we do them at the same time. We list the repo - directory and look at package.py files, and we compare the - index modification date with the ost recently modified package - file, storing the result. - - The implementation here should try to minimize filesystem - calls. At the moment, it is O(number of packages) and makes - about one stat call per package. This is resonably fast, and - avoids actually importing packages in Spack, which is slow. - - """ - if self._all_package_names is None: - self._all_package_names = [] - - # Get index modification time. - index_mtime = spack.misc_cache.mtime(self._cache_file) - - for pkg_name in os.listdir(self.packages_path): - # Skip non-directories in the package root. - pkg_dir = join_path(self.packages_path, pkg_name) - - # Warn about invalid names that look like packages. - if not valid_module_name(pkg_name): - msg = ("Skipping package at %s. " - "'%s' is not a valid Spack module name.") - tty.warn(msg % (pkg_dir, pkg_name)) - continue - - # construct the file name from the directory - pkg_file = join_path( - self.packages_path, pkg_name, package_file_name) - - # Use stat here to avoid lots of calls to the filesystem. - try: - sinfo = os.stat(pkg_file) - except OSError as e: - if e.errno == errno.ENOENT: - # No package.py file here. - continue - elif e.errno == errno.EACCES: - tty.warn("Can't read package file %s." % pkg_file) - continue - raise e - - # if it's not a file, skip it. - if stat.S_ISDIR(sinfo.st_mode): - continue - - # All checks passed. Add it to the list. - self._all_package_names.append(pkg_name) - - # record the package if it is newer than the index. - if sinfo.st_mtime > index_mtime: - self._needs_update.append(pkg_name) + def all_package_names(self): + """Returns a sorted list of all package names in the Repo.""" + return sorted(self._fast_package_checker.keys()) - self._all_package_names.sort() + def packages_with_tags(self, *tags): + v = set(self.all_package_names()) + index = self.tag_index - return self._all_package_names + for t in tags: + v &= set(index[t]) - def all_package_names(self): - """Returns a sorted list of all package names in the Repo.""" - self._fast_package_check() - return self._all_package_names + return sorted(v) def all_packages(self): """Iterator over all packages in the repository. @@ -768,16 +947,7 @@ class Repo(object): def exists(self, pkg_name): """Whether a package with the supplied name exists.""" - if self._all_package_names: - # This does a binary search in the sorted list. - idx = bisect_left(self.all_package_names(), pkg_name) - return (idx < len(self._all_package_names) and - self._all_package_names[idx] == pkg_name) - - # If we haven't generated the full package list, don't. - # Just check whether the file exists. - filename = self.filename_for_package_name(pkg_name) - return os.path.exists(filename) + return pkg_name in self._fast_package_checker def is_virtual(self, pkg_name): """True if the package with this name is virtual, False otherwise.""" diff --git a/lib/spack/spack/test/cmd/find.py b/lib/spack/spack/test/cmd/find.py index b082b71c06..57dd688362 100644 --- a/lib/spack/spack/test/cmd/find.py +++ b/lib/spack/spack/test/cmd/find.py @@ -22,12 +22,40 @@ # 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.cmd.find from spack.util.pattern import Bunch +@pytest.fixture(scope='module') +def parser(): + """Returns the parser for the module command""" + prs = argparse.ArgumentParser() + spack.cmd.find.setup_parser(prs) + return prs + + +@pytest.fixture() +def specs(): + s = [] + return s + + +@pytest.fixture() +def mock_display(monkeypatch, specs): + """Monkeypatches the display function to return its first argument""" + + def display(x, *args, **kwargs): + specs.extend(x) + + monkeypatch.setattr(spack.cmd.find, 'display_specs', display) + + def test_query_arguments(): query_arguments = spack.cmd.find.query_arguments + # Default arguments args = Bunch( only_missing=False, @@ -36,6 +64,7 @@ def test_query_arguments(): explicit=False, implicit=False ) + q_args = query_arguments(args) assert 'installed' in q_args assert 'known' in q_args @@ -43,11 +72,39 @@ def test_query_arguments(): assert q_args['installed'] is True assert q_args['known'] is any assert q_args['explicit'] is any + # Check that explicit works correctly args.explicit = True q_args = query_arguments(args) assert q_args['explicit'] is True + args.explicit = False args.implicit = True q_args = query_arguments(args) assert q_args['explicit'] is False + + +@pytest.mark.usefixtures('database', 'mock_display') +class TestFindWithTags(object): + + def test_tag1(self, parser, specs): + + args = parser.parse_args(['--tags', 'tag1']) + spack.cmd.find.find(parser, args) + + assert len(specs) == 2 + assert 'mpich' in [x.name for x in specs] + assert 'mpich2' in [x.name for x in specs] + + def test_tag2(self, parser, specs): + args = parser.parse_args(['--tags', 'tag2']) + spack.cmd.find.find(parser, args) + + assert len(specs) == 1 + assert 'mpich' in [x.name for x in specs] + + def test_tag2_tag3(self, parser, specs): + args = parser.parse_args(['--tags', 'tag2', '--tags', 'tag3']) + spack.cmd.find.find(parser, args) + + assert len(specs) == 0 diff --git a/lib/spack/spack/test/cmd/info.py b/lib/spack/spack/test/cmd/info.py index 9819f2cd84..6c71515a3f 100644 --- a/lib/spack/spack/test/cmd/info.py +++ b/lib/spack/spack/test/cmd/info.py @@ -22,13 +22,39 @@ # 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.cmd.info from spack.main import SpackCommand info = SpackCommand('info') +@pytest.fixture(scope='module') +def parser(): + """Returns the parser for the module command""" + prs = argparse.ArgumentParser() + spack.cmd.info.setup_parser(prs) + return prs + + +@pytest.fixture() +def info_lines(): + lines = [] + return lines + + +@pytest.fixture() +def mock_print(monkeypatch, info_lines): + + def _print(*args): + info_lines.extend(args) + + monkeypatch.setattr(spack.cmd.info.color, 'cprint', _print, raising=False) + + @pytest.mark.parametrize('pkg', [ 'openmpi', 'trilinos', @@ -38,3 +64,29 @@ info = SpackCommand('info') ]) def test_it_just_runs(pkg): info(pkg) + + +@pytest.mark.parametrize('pkg_query', [ + 'hdf5', + 'cloverleaf3d', + 'trilinos' +]) +@pytest.mark.usefixtures('mock_print') +def test_info_fields(pkg_query, parser, info_lines): + + expected_fields = ( + 'Description:', + 'Homepage:', + 'Safe versions:', + 'Variants:', + 'Installation Phases:', + 'Virtual Packages:', + 'Tags:' + ) + + args = parser.parse_args([pkg_query]) + spack.cmd.info.info(parser, args) + + for text in expected_fields: + match = [x for x in info_lines if text in x] + assert match diff --git a/lib/spack/spack/test/cmd/list.py b/lib/spack/spack/test/cmd/list.py new file mode 100644 index 0000000000..244ae5fcbc --- /dev/null +++ b/lib/spack/spack/test/cmd/list.py @@ -0,0 +1,73 @@ +############################################################################## +# Copyright (c) 2013-2016, Lawrence Livermore National Security, LLC. +# Produced at the Lawrence Livermore National Laboratory. +# +# This file is part of Spack. +# Created by Todd Gamblin, tgamblin@llnl.gov, All rights reserved. +# LLNL-CODE-647188 +# +# For details, see https://github.com/llnl/spack +# Please also see the NOTICE and LICENSE files for our notice and the LGPL. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License (as +# published by the Free Software Foundation) version 2.1, February 1999. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the IMPLIED WARRANTY OF +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the terms and +# conditions of the GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# 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.cmd.list + + +@pytest.fixture(scope='module') +def parser(): + """Returns the parser for the module command""" + prs = argparse.ArgumentParser() + spack.cmd.list.setup_parser(prs) + return prs + + +@pytest.fixture() +def pkg_names(): + pkg_names = [] + return pkg_names + + +@pytest.fixture() +def mock_name_only(monkeypatch, pkg_names): + + def name_only(x): + pkg_names.extend(x) + + monkeypatch.setattr(spack.cmd.list, 'name_only', name_only) + monkeypatch.setitem(spack.cmd.list.formatters, 'name_only', name_only) + + +@pytest.mark.usefixtures('mock_name_only') +class TestListCommand(object): + + def test_list_without_filters(self, parser, pkg_names): + + args = parser.parse_args([]) + spack.cmd.list.list(parser, args) + + assert pkg_names + assert 'cloverleaf3d' in pkg_names + assert 'hdf5' in pkg_names + + def test_list_with_filters(self, parser, pkg_names): + args = parser.parse_args(['--tags', 'proxy-app']) + spack.cmd.list.list(parser, args) + + assert pkg_names + assert 'cloverleaf3d' in pkg_names + assert 'hdf5' not in pkg_names \ No newline at end of file -- cgit v1.2.3-60-g2f50