From fd055d46784fdb9a2beecd1c81315e2bd2686c21 Mon Sep 17 00:00:00 2001 From: Tamara Dahlgren <35777542+tldahlgren@users.noreply.github.com> Date: Mon, 28 Mar 2022 15:15:38 -0700 Subject: spack info: make sections optional; add build/stand-alone test information (#22097) Add output of build- and install-time tests to info command Enable dependencies, variants, and versions by default (i.e., provide --no* options; add gcc to test_info_fields to increase coverage for c_names->v_names --- lib/spack/spack/cmd/info.py | 221 +++++++++++++++++++++++++++++++-------- lib/spack/spack/package.py | 10 +- lib/spack/spack/test/cmd/info.py | 7 +- 3 files changed, 190 insertions(+), 48 deletions(-) (limited to 'lib') diff --git a/lib/spack/spack/cmd/info.py b/lib/spack/spack/cmd/info.py index e3ca3ea5ef..f6273c4711 100644 --- a/lib/spack/spack/cmd/info.py +++ b/lib/spack/spack/cmd/info.py @@ -5,6 +5,7 @@ from __future__ import print_function +import inspect import textwrap from six.moves import zip_longest @@ -17,7 +18,7 @@ import spack.cmd.common.arguments as arguments import spack.fetch_strategy as fs import spack.repo import spack.spec -from spack.package import preferred_version +from spack.package import has_test_method, preferred_version description = 'get detailed information on a particular package' section = 'basic' @@ -39,6 +40,25 @@ def padder(str_list, extra=0): def setup_parser(subparser): + subparser.add_argument( + '-a', '--all', action='store_true', default=False, + help="output all package information" + ) + + options = [ + ('--detectable', print_detectable.__doc__), + ('--maintainers', print_maintainers.__doc__), + ('--no-dependencies', 'do not ' + print_dependencies.__doc__), + ('--no-variants', 'do not ' + print_variants.__doc__), + ('--no-versions', 'do not ' + print_versions.__doc__), + ('--phases', print_phases.__doc__), + ('--tags', print_tags.__doc__), + ('--tests', print_tests.__doc__), + ('--virtuals', print_virtuals.__doc__), + ] + for opt, help_comment in options: + subparser.add_argument(opt, action='store_true', help=help_comment) + arguments.add_common_arguments(subparser, ['package']) @@ -145,27 +165,21 @@ class VariantFormatter(object): yield " " + self.fmt % t -def print_text_info(pkg): - """Print out a plain text description of a package.""" +def print_dependencies(pkg): + """output build, link, and run package dependencies""" - header = section_title( - '{0}: ' - ).format(pkg.build_system_class) + pkg.name - color.cprint(header) - - color.cprint('') - color.cprint(section_title('Description:')) - if pkg.__doc__: - color.cprint(color.cescape(pkg.format_doc(indent=4))) - else: - color.cprint(" None") + for deptype in ('build', 'link', 'run'): + color.cprint('') + color.cprint(section_title('%s Dependencies:' % deptype.capitalize())) + deps = sorted(pkg.dependencies_of_type(deptype)) + if deps: + colify(deps, indent=4) + else: + color.cprint(' None') - color.cprint(section_title('Homepage: ') + pkg.homepage) - if len(pkg.maintainers) > 0: - mnt = " ".join(['@@' + m for m in pkg.maintainers]) - color.cprint('') - color.cprint(section_title('Maintainers: ') + mnt) +def print_detectable(pkg): + """output information on external detection""" color.cprint('') color.cprint(section_title('Externally Detectable: ')) @@ -187,6 +201,31 @@ def print_text_info(pkg): else: color.cprint(' False') + +def print_maintainers(pkg): + """output package maintainers""" + + if len(pkg.maintainers) > 0: + mnt = " ".join(['@@' + m for m in pkg.maintainers]) + color.cprint('') + color.cprint(section_title('Maintainers: ') + mnt) + + +def print_phases(pkg): + """output installation phases""" + + if hasattr(pkg, 'phases') and pkg.phases: + color.cprint('') + color.cprint(section_title('Installation Phases:')) + phase_str = '' + for phase in pkg.phases: + phase_str += " {0}".format(phase) + color.cprint(phase_str) + + +def print_tags(pkg): + """output package tags""" + color.cprint('') color.cprint(section_title("Tags: ")) if hasattr(pkg, 'tags'): @@ -195,6 +234,90 @@ def print_text_info(pkg): else: color.cprint(" None") + +def print_tests(pkg): + """output relevant build-time and stand-alone tests""" + + # Some built-in base packages (e.g., Autotools) define callback (e.g., + # check) inherited by descendant packages. These checks may not result + # in build-time testing if the package's build does not implement the + # expected functionality (e.g., a 'check' or 'test' targets). + # + # So the presence of a callback in Spack does not necessarily correspond + # to the actual presence of built-time tests for a package. + for callbacks, phase in [(pkg.build_time_test_callbacks, 'Build'), + (pkg.install_time_test_callbacks, 'Install')]: + color.cprint('') + color.cprint(section_title('Available {0} Phase Test Methods:' + .format(phase))) + names = [] + if callbacks: + for name in callbacks: + if getattr(pkg, name, False): + names.append(name) + + if names: + colify(sorted(names), indent=4) + else: + color.cprint(' None') + + # PackageBase defines an empty install/smoke test but we want to know + # if it has been overridden and, therefore, assumed to be implemented. + color.cprint('') + color.cprint(section_title('Stand-Alone/Smoke Test Methods:')) + names = [] + pkg_cls = pkg if inspect.isclass(pkg) else pkg.__class__ + if has_test_method(pkg_cls): + pkg_base = spack.package.PackageBase + test_pkgs = [str(cls.test) for cls in inspect.getmro(pkg_cls) if + issubclass(cls, pkg_base) and cls.test != pkg_base.test] + test_pkgs = list(set(test_pkgs)) + names.extend([(test.split()[1]).lower() for test in test_pkgs]) + + # TODO Refactor START + # Use code from package.py's test_process IF this functionality is + # accepted. + v_names = list(set([vspec.name for vspec in pkg.virtuals_provided])) + + # hack for compilers that are not dependencies (yet) + # TODO: this all eventually goes away + c_names = ('gcc', 'intel', 'intel-parallel-studio', 'pgi') + if pkg.name in c_names: + v_names.extend(['c', 'cxx', 'fortran']) + if pkg.spec.satisfies('llvm+clang'): + v_names.extend(['c', 'cxx']) + # TODO Refactor END + + v_specs = [spack.spec.Spec(v_name) for v_name in v_names] + for v_spec in v_specs: + try: + pkg = v_spec.package + pkg_cls = pkg if inspect.isclass(pkg) else pkg.__class__ + if has_test_method(pkg_cls): + names.append('{0}.test'.format(pkg.name.lower())) + except spack.repo.UnknownPackageError: + pass + + if names: + colify(sorted(names), indent=4) + else: + color.cprint(' None') + + +def print_variants(pkg): + """output variants""" + + color.cprint('') + color.cprint(section_title('Variants:')) + + formatter = VariantFormatter(pkg.variants) + for line in formatter.lines: + color.cprint(color.cescape(line)) + + +def print_versions(pkg): + """output versions""" + color.cprint('') color.cprint(section_title('Preferred version: ')) @@ -238,29 +361,9 @@ def print_text_info(pkg): line = version(' {0}'.format(pad(v))) + color.cescape(url) color.cprint(line) - color.cprint('') - color.cprint(section_title('Variants:')) - - formatter = VariantFormatter(pkg.variants) - for line in formatter.lines: - color.cprint(color.cescape(line)) - if hasattr(pkg, 'phases') and pkg.phases: - color.cprint('') - color.cprint(section_title('Installation Phases:')) - phase_str = '' - for phase in pkg.phases: - phase_str += " {0}".format(phase) - color.cprint(phase_str) - - for deptype in ('build', 'link', 'run'): - color.cprint('') - color.cprint(section_title('%s Dependencies:' % deptype.capitalize())) - deps = sorted(pkg.dependencies_of_type(deptype)) - if deps: - colify(deps, indent=4) - else: - color.cprint(' None') +def print_virtuals(pkg): + """output virtual packages""" color.cprint('') color.cprint(section_title('Virtual Packages: ')) @@ -280,9 +383,39 @@ def print_text_info(pkg): else: color.cprint(" None") - color.cprint('') - def info(parser, args): pkg = spack.repo.get(args.package) - print_text_info(pkg) + + # Output core package information + header = section_title( + '{0}: ' + ).format(pkg.build_system_class) + pkg.name + color.cprint(header) + + color.cprint('') + color.cprint(section_title('Description:')) + if pkg.__doc__: + color.cprint(color.cescape(pkg.format_doc(indent=4))) + else: + color.cprint(" None") + + color.cprint(section_title('Homepage: ') + pkg.homepage) + + # Now output optional information in expected order + sections = [ + (args.all or args.maintainers, print_maintainers), + (args.all or args.detectable, print_detectable), + (args.all or args.tags, print_tags), + (args.all or not args.no_versions, print_versions), + (args.all or not args.no_variants, print_variants), + (args.all or args.phases, print_phases), + (args.all or not args.no_dependencies, print_dependencies), + (args.all or args.virtuals, print_virtuals), + (args.all or args.tests, print_tests), + ] + for print_it, func in sections: + if print_it: + func(pkg) + + color.cprint('') diff --git a/lib/spack/spack/package.py b/lib/spack/spack/package.py index 4973a04ee2..e200985cdd 100644 --- a/lib/spack/spack/package.py +++ b/lib/spack/spack/package.py @@ -2710,7 +2710,15 @@ class PackageBase(six.with_metaclass(PackageMeta, PackageViewMixin, object)): def has_test_method(pkg): - """Returns True if the package defines its own stand-alone test method.""" + """Determine if the package defines its own stand-alone test method. + + Args: + pkg (str): the package being checked + + Returns: + (bool): ``True`` if the package overrides the default method; else + ``False`` + """ if not inspect.isclass(pkg): tty.die('{0}: is not a class, it is {1}'.format(pkg, type(pkg))) diff --git a/lib/spack/spack/test/cmd/info.py b/lib/spack/spack/test/cmd/info.py index 9d240628b4..a5e8ae0bd8 100644 --- a/lib/spack/spack/test/cmd/info.py +++ b/lib/spack/spack/test/cmd/info.py @@ -74,7 +74,7 @@ def test_info_noversion(mock_packages, info_lines, mock_print): ]) @pytest.mark.usefixtures('mock_print') def test_is_externally_detectable(pkg_query, expected, parser, info_lines): - args = parser.parse_args([pkg_query]) + args = parser.parse_args(['--detectable', pkg_query]) spack.cmd.info.info(parser, args) line_iter = info_lines.__iter__() @@ -87,7 +87,8 @@ def test_is_externally_detectable(pkg_query, expected, parser, info_lines): @pytest.mark.parametrize('pkg_query', [ 'hdf5', 'cloverleaf3d', - 'trilinos' + 'trilinos', + 'gcc' # This should ensure --test's c_names processing loop covered ]) @pytest.mark.usefixtures('mock_print') def test_info_fields(pkg_query, parser, info_lines): @@ -103,7 +104,7 @@ def test_info_fields(pkg_query, parser, info_lines): 'Tags:' ) - args = parser.parse_args([pkg_query]) + args = parser.parse_args(['--all', pkg_query]) spack.cmd.info.info(parser, args) for text in expected_fields: -- cgit v1.2.3-70-g09d2