summaryrefslogtreecommitdiff
path: root/lib/spack/spack/report.py
diff options
context:
space:
mode:
Diffstat (limited to 'lib/spack/spack/report.py')
-rw-r--r--lib/spack/spack/report.py267
1 files changed, 267 insertions, 0 deletions
diff --git a/lib/spack/spack/report.py b/lib/spack/spack/report.py
new file mode 100644
index 0000000000..74e5caf194
--- /dev/null
+++ b/lib/spack/spack/report.py
@@ -0,0 +1,267 @@
+# Copyright 2013-2018 Lawrence Livermore National Security, LLC and other
+# Spack Project Developers. See the top-level COPYRIGHT file for details.
+#
+# SPDX-License-Identifier: (Apache-2.0 OR MIT)
+
+"""Tools to produce reports of spec installations"""
+import codecs
+import collections
+import functools
+import time
+import traceback
+
+import llnl.util.lang
+import spack.build_environment
+import spack.fetch_strategy
+import spack.package
+from spack.reporter import Reporter
+from spack.reporters.cdash import CDash
+from spack.reporters.junit import JUnit
+
+report_writers = {
+ None: Reporter,
+ 'junit': JUnit,
+ 'cdash': CDash
+}
+
+#: Allowed report formats
+valid_formats = list(report_writers.keys())
+
+__all__ = [
+ 'valid_formats',
+ 'collect_info'
+]
+
+
+def fetch_package_log(pkg):
+ try:
+ with codecs.open(pkg.build_log_path, 'r', 'utf-8') as f:
+ return ''.join(f.readlines())
+ except Exception:
+ return 'Cannot open build log for {0}'.format(
+ pkg.spec.cshort_spec
+ )
+
+
+class InfoCollector(object):
+ """Decorates PackageBase.do_install to collect information
+ on the installation of certain specs.
+
+ When exiting the context this change will be rolled-back.
+
+ The data collected is available through the ``specs``
+ attribute once exited, and it's organized as a list where
+ each item represents the installation of one of the spec.
+
+ Args:
+ specs (list of Spec): specs whose install information will
+ be recorded
+ """
+ #: Backup of PackageBase.do_install
+ _backup_do_install = spack.package.PackageBase.do_install
+
+ def __init__(self, specs):
+ #: Specs that will be installed
+ self.input_specs = specs
+ #: This is where we record the data that will be included
+ #: in our report.
+ self.specs = []
+
+ def __enter__(self):
+ # Initialize the spec report with the data that is available upfront.
+ for input_spec in self.input_specs:
+ name_fmt = '{0}_{1}'
+ name = name_fmt.format(input_spec.name,
+ input_spec.dag_hash(length=7))
+
+ spec = {
+ 'name': name,
+ 'nerrors': None,
+ 'nfailures': None,
+ 'npackages': None,
+ 'time': None,
+ 'timestamp': time.strftime(
+ "%a, %d %b %Y %H:%M:%S", time.gmtime()
+ ),
+ 'properties': [],
+ 'packages': []
+ }
+
+ self.specs.append(spec)
+
+ Property = collections.namedtuple('Property', ['name', 'value'])
+ spec['properties'].append(
+ Property('architecture', input_spec.architecture)
+ )
+ spec['properties'].append(
+ Property('compiler', input_spec.compiler))
+
+ # Check which specs are already installed and mark them as skipped
+ for dep in filter(lambda x: x.package.installed,
+ input_spec.traverse()):
+ package = {
+ 'name': dep.name,
+ 'id': dep.dag_hash(),
+ 'elapsed_time': '0.0',
+ 'result': 'skipped',
+ 'message': 'Spec already installed'
+ }
+ spec['packages'].append(package)
+
+ def gather_info(do_install):
+ """Decorates do_install to gather useful information for
+ a CI report.
+
+ It's defined here to capture the environment and build
+ this context as the installations proceed.
+ """
+ @functools.wraps(do_install)
+ def wrapper(pkg, *args, **kwargs):
+
+ # We accounted before for what is already installed
+ installed_on_entry = pkg.installed
+
+ package = {
+ 'name': pkg.name,
+ 'id': pkg.spec.dag_hash(),
+ 'elapsed_time': None,
+ 'result': None,
+ 'message': None
+ }
+
+ start_time = time.time()
+ value = None
+ try:
+
+ value = do_install(pkg, *args, **kwargs)
+ package['result'] = 'success'
+ package['stdout'] = fetch_package_log(pkg)
+ if installed_on_entry:
+ return
+
+ except spack.build_environment.InstallError as e:
+ # An InstallError is considered a failure (the recipe
+ # didn't work correctly)
+ package['result'] = 'failure'
+ package['stdout'] = fetch_package_log(pkg)
+ package['message'] = e.message or 'Installation failure'
+ package['exception'] = e.traceback
+
+ except (Exception, BaseException) as e:
+ # Everything else is an error (the installation
+ # failed outside of the child process)
+ package['result'] = 'error'
+ package['stdout'] = fetch_package_log(pkg)
+ package['message'] = str(e) or 'Unknown error'
+ package['exception'] = traceback.format_exc()
+
+ finally:
+ package['elapsed_time'] = time.time() - start_time
+
+ # Append the package to the correct spec report. In some
+ # cases it may happen that a spec that is asked to be
+ # installed explicitly will also be installed as a
+ # dependency of another spec. In this case append to both
+ # spec reports.
+ for s in llnl.util.lang.dedupe([pkg.spec.root, pkg.spec]):
+ name = name_fmt.format(s.name, s.dag_hash(length=7))
+ try:
+ item = next((
+ x for x in self.specs
+ if x['name'] == name
+ ))
+ item['packages'].append(package)
+ except StopIteration:
+ pass
+
+ return value
+
+ return wrapper
+
+ spack.package.PackageBase.do_install = gather_info(
+ spack.package.PackageBase.do_install
+ )
+
+ def __exit__(self, exc_type, exc_val, exc_tb):
+
+ # Restore the original method in PackageBase
+ spack.package.PackageBase.do_install = InfoCollector._backup_do_install
+
+ for spec in self.specs:
+ spec['npackages'] = len(spec['packages'])
+ spec['nfailures'] = len(
+ [x for x in spec['packages'] if x['result'] == 'failure']
+ )
+ spec['nerrors'] = len(
+ [x for x in spec['packages'] if x['result'] == 'error']
+ )
+ spec['time'] = sum([
+ float(x['elapsed_time']) for x in spec['packages']
+ ])
+
+
+class collect_info(object):
+ """Collects information to build a report while installing
+ and dumps it on exit.
+
+ If the format name is not ``None``, this context manager
+ decorates PackageBase.do_install when entering the context
+ and unrolls the change when exiting.
+
+ Within the context, only the specs that are passed to it
+ on initialization will be recorded for the report. Data from
+ other specs will be discarded.
+
+ Examples:
+
+ .. code-block:: python
+
+ # The file 'junit.xml' is written when exiting
+ # the context
+ specs = [Spec('hdf5').concretized()]
+ with collect_info(specs, 'junit', 'junit.xml'):
+ # A report will be generated for these specs...
+ for spec in specs:
+ spec.do_install()
+ # ...but not for this one
+ Spec('zlib').concretized().do_install()
+
+ Args:
+ format_name (str or None): one of the supported formats
+ install_command (str): the command line passed to spack
+ cdash_upload_url (str or None): where to upload the report
+
+ Raises:
+ ValueError: when ``format_name`` is not in ``valid_formats``
+ """
+ def __init__(self, format_name, install_command, cdash_upload_url):
+ self.filename = None
+ if cdash_upload_url:
+ self.format_name = 'cdash'
+ self.filename = 'cdash_report'
+ else:
+ self.format_name = format_name
+ # Check that the format is valid.
+ if self.format_name not in valid_formats:
+ raise ValueError('invalid report type: {0}'
+ .format(self.format_name))
+ self.report_writer = report_writers[self.format_name](
+ install_command, cdash_upload_url)
+
+ def concretization_report(self, msg):
+ self.report_writer.concretization_report(self.filename, msg)
+
+ def __enter__(self):
+ if self.format_name:
+ # Start the collector and patch PackageBase.do_install
+ self.collector = InfoCollector(self.specs)
+ self.collector.__enter__()
+
+ def __exit__(self, exc_type, exc_val, exc_tb):
+ if self.format_name:
+ # Close the collector and restore the
+ # original PackageBase.do_install
+ self.collector.__exit__(exc_type, exc_val, exc_tb)
+
+ report_data = {'specs': self.collector.specs}
+ self.report_writer.build_report(self.filename, report_data)