From e73ab8468024ad23e2624fcf1db69276947ddc7b Mon Sep 17 00:00:00 2001 From: Massimiliano Culpo Date: Wed, 26 Oct 2016 23:22:46 +0200 Subject: spack install : added --log-format option (incorporates test-install command) (#2112) * spack install : added --log-format option (incorporates test-install command) fixes #1907 * qa : removed extra whitespace --- lib/spack/spack/cmd/install.py | 231 ++++++++++++++++++++++++++++- lib/spack/spack/cmd/test_install.py | 245 ------------------------------- lib/spack/spack/package.py | 4 +- lib/spack/spack/test/__init__.py | 2 +- lib/spack/spack/test/cmd/install.py | 213 +++++++++++++++++++++++++++ lib/spack/spack/test/cmd/test_install.py | 210 -------------------------- 6 files changed, 446 insertions(+), 459 deletions(-) delete mode 100644 lib/spack/spack/cmd/test_install.py create mode 100644 lib/spack/spack/test/cmd/install.py delete mode 100644 lib/spack/spack/test/cmd/test_install.py diff --git a/lib/spack/spack/cmd/install.py b/lib/spack/spack/cmd/install.py index aab7c0abc7..417e07e9c4 100644 --- a/lib/spack/spack/cmd/install.py +++ b/lib/spack/spack/cmd/install.py @@ -23,11 +23,20 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA ############################################################################## import argparse +import codecs +import functools +import os +import time +import xml.dom.minidom +import xml.etree.ElementTree as ET +import llnl.util.filesystem as fs import llnl.util.tty as tty - import spack import spack.cmd +from spack.build_environment import InstallError +from spack.fetch_strategy import FetchError +from spack.package import PackageBase description = "Build and install packages" @@ -71,7 +80,207 @@ the dependencies.""" ) subparser.add_argument( '--run-tests', action='store_true', dest='run_tests', - help="Run tests during installation of a package.") + help="Run package level tests during installation." + ) + subparser.add_argument( + '--log-format', + default=None, + choices=['junit'], + help="Format to be used for log files." + ) + subparser.add_argument( + '--log-file', + default=None, + help="Filename for the log file. If not passed a default will be used." + ) + + +# Needed for test cases +class TestResult(object): + PASSED = 0 + FAILED = 1 + SKIPPED = 2 + ERRORED = 3 + + +class TestSuite(object): + def __init__(self): + self.root = ET.Element('testsuite') + self.tests = [] + + def append(self, item): + if not isinstance(item, TestCase): + raise TypeError( + 'only TestCase instances may be appended to TestSuite' + ) + self.tests.append(item) # Append the item to the list of tests + + def dump(self, filename): + # Prepare the header for the entire test suite + number_of_errors = sum( + x.result_type == TestResult.ERRORED for x in self.tests + ) + self.root.set('errors', str(number_of_errors)) + number_of_failures = sum( + x.result_type == TestResult.FAILED for x in self.tests + ) + self.root.set('failures', str(number_of_failures)) + self.root.set('tests', str(len(self.tests))) + + for item in self.tests: + self.root.append(item.element) + + with codecs.open(filename, 'wb', 'utf-8') as file: + xml_string = ET.tostring(self.root) + xml_string = xml.dom.minidom.parseString(xml_string).toprettyxml() + file.write(xml_string) + + +class TestCase(object): + + results = { + TestResult.PASSED: None, + TestResult.SKIPPED: 'skipped', + TestResult.FAILED: 'failure', + TestResult.ERRORED: 'error', + } + + def __init__(self, classname, name): + self.element = ET.Element('testcase') + self.element.set('classname', str(classname)) + self.element.set('name', str(name)) + self.result_type = None + + def set_duration(self, duration): + self.element.set('time', str(duration)) + + def set_result(self, result_type, + message=None, error_type=None, text=None): + self.result_type = result_type + result = TestCase.results[self.result_type] + if result is not None and result is not TestResult.PASSED: + subelement = ET.SubElement(self.element, result) + if error_type is not None: + subelement.set('type', error_type) + if message is not None: + subelement.set('message', str(message)) + if text is not None: + subelement.text = text + + +def fetch_text(path): + if not os.path.exists(path): + return '' + + with codecs.open(path, 'rb', 'utf-8') as f: + return '\n'.join( + list(line.strip() for line in f.readlines()) + ) + + +def junit_output(spec, test_suite): + # Cycle once and for all on the dependencies and skip + # the ones that are already installed. This ensures that + # for the same spec, the same number of entries will be + # displayed in the XML report + for x in spec.traverse(order='post'): + package = spack.repo.get(x) + if package.installed: + test_case = TestCase(package.name, x.short_spec) + test_case.set_duration(0.0) + test_case.set_result( + TestResult.SKIPPED, + message='Skipped [already installed]', + error_type='already_installed' + ) + test_suite.append(test_case) + + def decorator(func): + @functools.wraps(func) + def wrapper(self, *args, ** kwargs): + + # Check if the package has been installed already + if self.installed: + return + + test_case = TestCase(self.name, self.spec.short_spec) + # Try to install the package + try: + # If already installed set the spec as skipped + start_time = time.time() + # PackageBase.do_install + func(self, *args, **kwargs) + duration = time.time() - start_time + test_case.set_duration(duration) + test_case.set_result(TestResult.PASSED) + except InstallError: + # Check if the package relies on dependencies that + # did not install + duration = time.time() - start_time + test_case.set_duration(duration) + if [x for x in self.spec.dependencies(('link', 'run')) if not spack.repo.get(x).installed]: # NOQA: ignore=E501 + test_case.set_duration(0.0) + test_case.set_result( + TestResult.SKIPPED, + message='Skipped [failed dependencies]', + error_type='dep_failed' + ) + else: + # An InstallError is considered a failure (the recipe + # didn't work correctly) + text = fetch_text(self.build_log_path) + test_case.set_result( + TestResult.FAILED, + message='Installation failure', + text=text + ) + except FetchError: + # A FetchError is considered an error as + # we didn't even start building + duration = time.time() - start_time + test_case.set_duration(duration) + text = fetch_text(self.build_log_path) + test_case.set_result( + TestResult.ERRORED, + message='Unable to fetch package', + text=text + ) + except Exception: + # Anything else is also an error + duration = time.time() - start_time + test_case.set_duration(duration) + text = fetch_text(self.build_log_path) + test_case.set_result( + TestResult.ERRORED, + message='Unexpected exception thrown during install', + text=text + ) + except: + # Anything else is also an error + duration = time.time() - start_time + test_case.set_duration(duration) + text = fetch_text(self.build_log_path) + test_case.set_result( + TestResult.ERRORED, + message='Unknown error', + text=text + ) + + # Try to get the log + test_suite.append(test_case) + return wrapper + return decorator + + +def default_log_file(spec): + """Computes the default filename for the log file and creates + the corresponding directory if not present + """ + fmt = 'test-{x.name}-{x.version}-{hash}.xml' + basename = fmt.format(x=spec, hash=spec.dag_hash()) + dirname = fs.join_path(spack.var_path, 'junit-report') + fs.mkdirp(dirname) + return fs.join_path(dirname, basename) def install(parser, args, **kwargs): @@ -104,6 +313,20 @@ def install(parser, args, **kwargs): tty.error('only one spec can be installed at a time.') spec = specs.pop() + # Check if we were asked to produce some log for dashboards + if args.log_format is not None: + # Compute the filename for logging + log_filename = args.log_file + if not log_filename: + log_filename = default_log_file(spec) + # Create the test suite in which to log results + test_suite = TestSuite() + # Decorate PackageBase.do_install to get installation status + PackageBase.do_install = junit_output( + spec, test_suite + )(PackageBase.do_install) + + # Do the actual installation if args.things_to_install == 'dependencies': # Install dependencies as-if they were installed # for root (explicit=False in the DB) @@ -115,3 +338,7 @@ def install(parser, args, **kwargs): package = spack.repo.get(spec) kwargs['explicit'] = True package.do_install(**kwargs) + + # Dump log file if asked to + if args.log_format is not None: + test_suite.dump(log_filename) diff --git a/lib/spack/spack/cmd/test_install.py b/lib/spack/spack/cmd/test_install.py deleted file mode 100644 index f962c5988a..0000000000 --- a/lib/spack/spack/cmd/test_install.py +++ /dev/null @@ -1,245 +0,0 @@ -############################################################################## -# 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 LICENSE file 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 codecs -import os -import time -import xml.dom.minidom -import xml.etree.ElementTree as ET - -import llnl.util.tty as tty -import spack -import spack.cmd -from llnl.util.filesystem import * -from spack.build_environment import InstallError -from spack.fetch_strategy import FetchError - -description = "Run package install as a unit test, output formatted results." - - -def setup_parser(subparser): - subparser.add_argument( - '-j', '--jobs', action='store', type=int, - help="Explicitly set number of make jobs. Default is #cpus.") - - subparser.add_argument( - '-n', '--no-checksum', action='store_true', dest='no_checksum', - help="Do not check packages against checksum") - - subparser.add_argument( - '-o', '--output', action='store', - help="test output goes in this file") - - subparser.add_argument( - 'package', nargs=argparse.REMAINDER, - help="spec of package to install") - - -class TestResult(object): - PASSED = 0 - FAILED = 1 - SKIPPED = 2 - ERRORED = 3 - - -class TestSuite(object): - - def __init__(self, filename): - self.filename = filename - self.root = ET.Element('testsuite') - self.tests = [] - - def __enter__(self): - return self - - def append(self, item): - if not isinstance(item, TestCase): - raise TypeError( - 'only TestCase instances may be appended to TestSuite') - self.tests.append(item) # Append the item to the list of tests - - def __exit__(self, exc_type, exc_val, exc_tb): - # Prepare the header for the entire test suite - number_of_errors = sum( - x.result_type == TestResult.ERRORED for x in self.tests) - self.root.set('errors', str(number_of_errors)) - number_of_failures = sum( - x.result_type == TestResult.FAILED for x in self.tests) - self.root.set('failures', str(number_of_failures)) - self.root.set('tests', str(len(self.tests))) - - for item in self.tests: - self.root.append(item.element) - - with open(self.filename, 'wb') as file: - xml_string = ET.tostring(self.root) - xml_string = xml.dom.minidom.parseString(xml_string).toprettyxml() - file.write(xml_string) - - -class TestCase(object): - - results = { - TestResult.PASSED: None, - TestResult.SKIPPED: 'skipped', - TestResult.FAILED: 'failure', - TestResult.ERRORED: 'error', - } - - def __init__(self, classname, name, time=None): - self.element = ET.Element('testcase') - self.element.set('classname', str(classname)) - self.element.set('name', str(name)) - if time is not None: - self.element.set('time', str(time)) - self.result_type = None - - def set_result(self, result_type, - message=None, error_type=None, text=None): - self.result_type = result_type - result = TestCase.results[self.result_type] - if result is not None and result is not TestResult.PASSED: - subelement = ET.SubElement(self.element, result) - if error_type is not None: - subelement.set('type', error_type) - if message is not None: - subelement.set('message', str(message)) - if text is not None: - subelement.text = text - - -def fetch_log(path): - if not os.path.exists(path): - return list() - with codecs.open(path, 'rb', 'utf-8') as F: - return list(line.strip() for line in F.readlines()) - - -def failed_dependencies(spec): - def get_deps(deptype): - return set(item for item in spec.dependencies(deptype) - if not spack.repo.get(item).installed) - link_deps = get_deps('link') - run_deps = get_deps('run') - return link_deps.union(run_deps) - - -def get_top_spec_or_die(args): - specs = spack.cmd.parse_specs(args.package, concretize=True) - if len(specs) > 1: - tty.die("Only 1 top-level package can be specified") - top_spec = iter(specs).next() - return top_spec - - -def install_single_spec(spec, number_of_jobs): - package = spack.repo.get(spec) - - # If it is already installed, skip the test - if spack.repo.get(spec).installed: - testcase = TestCase(package.name, package.spec.short_spec, time=0.0) - testcase.set_result( - TestResult.SKIPPED, - message='Skipped [already installed]', - error_type='already_installed') - return testcase - - # If it relies on dependencies that did not install, skip - if failed_dependencies(spec): - testcase = TestCase(package.name, package.spec.short_spec, time=0.0) - testcase.set_result( - TestResult.SKIPPED, - message='Skipped [failed dependencies]', - error_type='dep_failed') - return testcase - - # Otherwise try to install the spec - try: - start_time = time.time() - package.do_install(keep_prefix=False, - keep_stage=True, - install_deps=True, - make_jobs=number_of_jobs, - verbose=True, - fake=False) - duration = time.time() - start_time - testcase = TestCase(package.name, package.spec.short_spec, duration) - testcase.set_result(TestResult.PASSED) - except InstallError: - # An InstallError is considered a failure (the recipe didn't work - # correctly) - duration = time.time() - start_time - # Try to get the log - lines = fetch_log(package.build_log_path) - text = '\n'.join(lines) - testcase = TestCase(package.name, package.spec.short_spec, duration) - testcase.set_result(TestResult.FAILED, - message='Installation failure', text=text) - - except FetchError: - # A FetchError is considered an error (we didn't even start building) - duration = time.time() - start_time - testcase = TestCase(package.name, package.spec.short_spec, duration) - testcase.set_result(TestResult.ERRORED, - message='Unable to fetch package') - - return testcase - - -def get_filename(args, top_spec): - if not args.output: - fname = 'test-{x.name}-{x.version}-{hash}.xml'.format( - x=top_spec, hash=top_spec.dag_hash()) - output_directory = join_path(os.getcwd(), 'test-output') - if not os.path.exists(output_directory): - os.mkdir(output_directory) - output_filename = join_path(output_directory, fname) - else: - output_filename = args.output - return output_filename - - -def test_install(parser, args): - # Check the input - if not args.package: - tty.die("install requires a package argument") - - if args.jobs is not None: - if args.jobs <= 0: - tty.die("The -j option must be a positive integer!") - - if args.no_checksum: - spack.do_checksum = False # TODO: remove this global. - - # Get the one and only top spec - top_spec = get_top_spec_or_die(args) - # Get the filename of the test - output_filename = get_filename(args, top_spec) - # TEST SUITE - with TestSuite(output_filename) as test_suite: - # Traverse in post order : each spec is a test case - for spec in top_spec.traverse(order='post'): - test_case = install_single_spec(spec, args.jobs) - test_suite.append(test_case) diff --git a/lib/spack/spack/package.py b/lib/spack/spack/package.py index 52dbd40f6f..3a3028885f 100644 --- a/lib/spack/spack/package.py +++ b/lib/spack/spack/package.py @@ -1180,7 +1180,9 @@ class PackageBase(object): verbose=verbose, make_jobs=make_jobs, run_tests=run_tests, - dirty=dirty) + dirty=dirty, + **kwargs + ) # Set run_tests flag before starting build. self.run_tests = run_tests diff --git a/lib/spack/spack/test/__init__.py b/lib/spack/spack/test/__init__.py index f6847d2929..457e5db9dc 100644 --- a/lib/spack/spack/test/__init__.py +++ b/lib/spack/spack/test/__init__.py @@ -41,7 +41,7 @@ test_names = [ 'cc', 'cmd.find', 'cmd.module', - 'cmd.test_install', + 'cmd.install', 'cmd.uninstall', 'concretize', 'concretize_preferences', diff --git a/lib/spack/spack/test/cmd/install.py b/lib/spack/spack/test/cmd/install.py new file mode 100644 index 0000000000..591bf02340 --- /dev/null +++ b/lib/spack/spack/test/cmd/install.py @@ -0,0 +1,213 @@ +############################################################################## +# 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 LICENSE file 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 StringIO +import argparse +import codecs +import collections +import contextlib +import unittest + +import llnl.util.filesystem +import spack +import spack.cmd +import spack.cmd.install as install + +FILE_REGISTRY = collections.defaultdict(StringIO.StringIO) + + +# 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.StringIO() + + try: + yield FILE_REGISTRY[filename] + finally: + handle = FILE_REGISTRY[filename] + FILE_REGISTRY[filename] = handle.getvalue() + handle.close() + + +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 _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 + + 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)]) + + +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.DependencySpec(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) diff --git a/lib/spack/spack/test/cmd/test_install.py b/lib/spack/spack/test/cmd/test_install.py deleted file mode 100644 index 4734fe1267..0000000000 --- a/lib/spack/spack/test/cmd/test_install.py +++ /dev/null @@ -1,210 +0,0 @@ -############################################################################## -# 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 LICENSE file 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 StringIO -import collections -import os -import unittest -import contextlib - -import spack -import spack.cmd -from spack.cmd import test_install - -FILE_REGISTRY = collections.defaultdict(StringIO.StringIO) - - -# Monkey-patch open to write module files to a StringIO instance -@contextlib.contextmanager -def mock_open(filename, mode): - if not mode == 'wb': - message = 'test.test_install : unexpected opening mode for mock_open' - raise RuntimeError(message) - - FILE_REGISTRY[filename] = StringIO.StringIO() - - try: - yield FILE_REGISTRY[filename] - finally: - handle = FILE_REGISTRY[filename] - FILE_REGISTRY[filename] = handle.getvalue() - handle.close() - - -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 _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 - - 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)]) - - -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): - 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.DependencySpec(specY, spack.alldeps) -pkgX = MockPackage(specX, 'logX') -pkgY = MockPackage(specY, 'logY') - - -class MockArgs(object): - - def __init__(self, package): - self.package = package - self.jobs = None - self.no_checksum = False - self.output = None - - -# TODO: add test(s) where Y fails to install -class TestInstallTest(unittest.TestCase): - """ - Tests test-install where X->Y - """ - - def setUp(self): - super(TestInstallTest, self).setUp() - - # 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.os_mkdir = os.mkdir - os.mkdir = lambda x: True - - # Monkey patch open - test_install.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 - test_install.open = open - - # Remove the monkey patched os.mkdir - os.mkdir = self.os_mkdir - del self.os_mkdir - - # Remove the monkey patched parse_specs - spack.cmd.parse_specs = self.parse_specs - del self.parse_specs - super(TestInstallTest, self).tearDown() - - spack.repo = self.saved_db - - def test_installing_both(self): - test_install.test_install(None, MockArgs('X')) - 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 - test_install.test_install(None, MockArgs('X')) - 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) -- cgit v1.2.3-60-g2f50