specs: better lookup by hash; allow references to missing dependency hashes
- previously spec parsing didn't allow you to look up missing (but still known) specs by hash - This allows you to reference and potentially reinstall force-uninstalled dependencies - add testing for force uninstall and for reference by spec - cmd/install tests now use mutable_database
This commit is contained in:
parent
c141e99e06
commit
6b619daef3
@ -937,6 +937,73 @@ def activated_extensions_for(self, extendee_spec, extensions_layout=None):
|
|||||||
continue
|
continue
|
||||||
# TODO: conditional way to do this instead of catching exceptions
|
# TODO: conditional way to do this instead of catching exceptions
|
||||||
|
|
||||||
|
def get_by_hash_local(self, dag_hash, default=None, installed=any):
|
||||||
|
"""Look up a spec in *this DB* by DAG hash, or by a DAG hash prefix.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
dag_hash (str): hash (or hash prefix) to look up
|
||||||
|
default (object, optional): default value to return if dag_hash is
|
||||||
|
not in the DB (default: None)
|
||||||
|
installed (bool or any, optional): if ``True``, includes only
|
||||||
|
installed specs in the search; if ``False`` only missing specs,
|
||||||
|
and if ``any``, either installed or missing (default: any)
|
||||||
|
|
||||||
|
``installed`` defaults to ``any`` so that we can refer to any
|
||||||
|
known hash. Note that ``query()`` and ``query_one()`` differ in
|
||||||
|
that they only return installed specs by default.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(list): a list of specs matching the hash or hash prefix
|
||||||
|
|
||||||
|
"""
|
||||||
|
with self.read_transaction():
|
||||||
|
# hash is a full hash and is in the data somewhere
|
||||||
|
if dag_hash in self._data:
|
||||||
|
rec = self._data[dag_hash]
|
||||||
|
if installed is any or rec.installed == installed:
|
||||||
|
return [rec.spec]
|
||||||
|
else:
|
||||||
|
return default
|
||||||
|
|
||||||
|
# check if hash is a prefix of some installed (or previously
|
||||||
|
# installed) spec.
|
||||||
|
matches = [record.spec for h, record in self._data.items()
|
||||||
|
if h.startswith(dag_hash) and
|
||||||
|
(installed is any or installed == record.installed)]
|
||||||
|
if matches:
|
||||||
|
return matches
|
||||||
|
|
||||||
|
# nothing found
|
||||||
|
return default
|
||||||
|
|
||||||
|
def get_by_hash(self, dag_hash, default=None, installed=any):
|
||||||
|
"""Look up a spec by DAG hash, or by a DAG hash prefix.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
dag_hash (str): hash (or hash prefix) to look up
|
||||||
|
default (object, optional): default value to return if dag_hash is
|
||||||
|
not in the DB (default: None)
|
||||||
|
installed (bool or any, optional): if ``True``, includes only
|
||||||
|
installed specs in the search; if ``False`` only missing specs,
|
||||||
|
and if ``any``, either installed or missing (default: any)
|
||||||
|
|
||||||
|
``installed`` defaults to ``any`` so that we can refer to any
|
||||||
|
known hash. Note that ``query()`` and ``query_one()`` differ in
|
||||||
|
that they only return installed specs by default.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(list): a list of specs matching the hash or hash prefix
|
||||||
|
|
||||||
|
"""
|
||||||
|
search_path = [self] + self.upstream_dbs
|
||||||
|
for db in search_path:
|
||||||
|
spec = db.get_by_hash_local(
|
||||||
|
dag_hash, default=default, installed=installed)
|
||||||
|
if spec is not None:
|
||||||
|
return spec
|
||||||
|
|
||||||
|
return default
|
||||||
|
|
||||||
def _query(
|
def _query(
|
||||||
self,
|
self,
|
||||||
query_spec=any,
|
query_spec=any,
|
||||||
|
@ -3951,17 +3951,15 @@ def parse_compiler(self, text):
|
|||||||
def spec_by_hash(self):
|
def spec_by_hash(self):
|
||||||
self.expect(ID)
|
self.expect(ID)
|
||||||
|
|
||||||
specs = spack.store.db.query()
|
dag_hash = self.token.value
|
||||||
matches = [spec for spec in specs if
|
matches = spack.store.db.get_by_hash(dag_hash)
|
||||||
spec.dag_hash()[:len(self.token.value)] == self.token.value]
|
|
||||||
|
|
||||||
if not matches:
|
if not matches:
|
||||||
raise NoSuchHashError(self.token.value)
|
raise NoSuchHashError(dag_hash)
|
||||||
|
|
||||||
if len(matches) != 1:
|
if len(matches) != 1:
|
||||||
raise AmbiguousHashError(
|
raise AmbiguousHashError(
|
||||||
"Multiple packages specify hash beginning '%s'."
|
"Multiple packages specify hash beginning '%s'."
|
||||||
% self.token.value, *matches)
|
% dag_hash, *matches)
|
||||||
|
|
||||||
return matches[0]
|
return matches[0]
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
from spack.main import SpackCommand, SpackCommandError
|
from spack.main import SpackCommand, SpackCommandError
|
||||||
|
|
||||||
uninstall = SpackCommand('uninstall')
|
uninstall = SpackCommand('uninstall')
|
||||||
|
install = SpackCommand('install')
|
||||||
|
|
||||||
|
|
||||||
class MockArgs(object):
|
class MockArgs(object):
|
||||||
@ -21,24 +22,21 @@ def __init__(self, packages, all=False, force=False, dependents=False):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.db
|
@pytest.mark.db
|
||||||
@pytest.mark.usefixtures('database')
|
def test_multiple_matches(mutable_database):
|
||||||
def test_multiple_matches():
|
|
||||||
"""Test unable to uninstall when multiple matches."""
|
"""Test unable to uninstall when multiple matches."""
|
||||||
with pytest.raises(SpackCommandError):
|
with pytest.raises(SpackCommandError):
|
||||||
uninstall('-y', 'mpileaks')
|
uninstall('-y', 'mpileaks')
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.db
|
@pytest.mark.db
|
||||||
@pytest.mark.usefixtures('database')
|
def test_installed_dependents(mutable_database):
|
||||||
def test_installed_dependents():
|
|
||||||
"""Test can't uninstall when ther are installed dependents."""
|
"""Test can't uninstall when ther are installed dependents."""
|
||||||
with pytest.raises(SpackCommandError):
|
with pytest.raises(SpackCommandError):
|
||||||
uninstall('-y', 'libelf')
|
uninstall('-y', 'libelf')
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.db
|
@pytest.mark.db
|
||||||
@pytest.mark.usefixtures('mutable_database')
|
def test_recursive_uninstall(mutable_database):
|
||||||
def test_recursive_uninstall():
|
|
||||||
"""Test recursive uninstall."""
|
"""Test recursive uninstall."""
|
||||||
uninstall('-y', '-a', '--dependents', 'callpath')
|
uninstall('-y', '-a', '--dependents', 'callpath')
|
||||||
|
|
||||||
@ -56,12 +54,11 @@ def test_recursive_uninstall():
|
|||||||
|
|
||||||
@pytest.mark.db
|
@pytest.mark.db
|
||||||
@pytest.mark.regression('3690')
|
@pytest.mark.regression('3690')
|
||||||
@pytest.mark.usefixtures('mutable_database')
|
|
||||||
@pytest.mark.parametrize('constraint,expected_number_of_specs', [
|
@pytest.mark.parametrize('constraint,expected_number_of_specs', [
|
||||||
('dyninst', 7), ('libelf', 5)
|
('dyninst', 7), ('libelf', 5)
|
||||||
])
|
])
|
||||||
def test_uninstall_spec_with_multiple_roots(
|
def test_uninstall_spec_with_multiple_roots(
|
||||||
constraint, expected_number_of_specs
|
constraint, expected_number_of_specs, mutable_database
|
||||||
):
|
):
|
||||||
uninstall('-y', '-a', '--dependents', constraint)
|
uninstall('-y', '-a', '--dependents', constraint)
|
||||||
|
|
||||||
@ -70,14 +67,91 @@ def test_uninstall_spec_with_multiple_roots(
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.db
|
@pytest.mark.db
|
||||||
@pytest.mark.usefixtures('mutable_database')
|
|
||||||
@pytest.mark.parametrize('constraint,expected_number_of_specs', [
|
@pytest.mark.parametrize('constraint,expected_number_of_specs', [
|
||||||
('dyninst', 13), ('libelf', 13)
|
('dyninst', 13), ('libelf', 13)
|
||||||
])
|
])
|
||||||
def test_force_uninstall_spec_with_ref_count_not_zero(
|
def test_force_uninstall_spec_with_ref_count_not_zero(
|
||||||
constraint, expected_number_of_specs
|
constraint, expected_number_of_specs, mutable_database
|
||||||
):
|
):
|
||||||
uninstall('-f', '-y', constraint)
|
uninstall('-f', '-y', constraint)
|
||||||
|
|
||||||
all_specs = spack.store.layout.all_specs()
|
all_specs = spack.store.layout.all_specs()
|
||||||
assert len(all_specs) == expected_number_of_specs
|
assert len(all_specs) == expected_number_of_specs
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.db
|
||||||
|
def test_force_uninstall_and_reinstall_by_hash(mutable_database):
|
||||||
|
"""Test forced uninstall and reinstall of old specs."""
|
||||||
|
# this is the spec to be removed
|
||||||
|
callpath_spec = spack.store.db.query_one('callpath ^mpich')
|
||||||
|
dag_hash = callpath_spec.dag_hash()
|
||||||
|
|
||||||
|
# ensure can look up by hash and that it's a dependent of mpileaks
|
||||||
|
def validate_callpath_spec(installed):
|
||||||
|
assert installed is True or installed is False
|
||||||
|
|
||||||
|
specs = spack.store.db.get_by_hash(dag_hash, installed=installed)
|
||||||
|
assert len(specs) == 1 and specs[0] == callpath_spec
|
||||||
|
|
||||||
|
specs = spack.store.db.get_by_hash(dag_hash[:7], installed=installed)
|
||||||
|
assert len(specs) == 1 and specs[0] == callpath_spec
|
||||||
|
|
||||||
|
specs = spack.store.db.get_by_hash(dag_hash, installed=any)
|
||||||
|
assert len(specs) == 1 and specs[0] == callpath_spec
|
||||||
|
|
||||||
|
specs = spack.store.db.get_by_hash(dag_hash[:7], installed=any)
|
||||||
|
assert len(specs) == 1 and specs[0] == callpath_spec
|
||||||
|
|
||||||
|
specs = spack.store.db.get_by_hash(dag_hash, installed=not installed)
|
||||||
|
assert specs is None
|
||||||
|
|
||||||
|
specs = spack.store.db.get_by_hash(dag_hash[:7],
|
||||||
|
installed=not installed)
|
||||||
|
assert specs is None
|
||||||
|
|
||||||
|
mpileaks_spec = spack.store.db.query_one('mpileaks ^mpich')
|
||||||
|
assert callpath_spec in mpileaks_spec
|
||||||
|
|
||||||
|
spec = spack.store.db.query_one('callpath ^mpich', installed=installed)
|
||||||
|
assert spec == callpath_spec
|
||||||
|
|
||||||
|
spec = spack.store.db.query_one('callpath ^mpich', installed=any)
|
||||||
|
assert spec == callpath_spec
|
||||||
|
|
||||||
|
spec = spack.store.db.query_one('callpath ^mpich',
|
||||||
|
installed=not installed)
|
||||||
|
assert spec is None
|
||||||
|
|
||||||
|
validate_callpath_spec(True)
|
||||||
|
|
||||||
|
uninstall('-y', '-f', 'callpath ^mpich')
|
||||||
|
|
||||||
|
# ensure that you can still look up by hash and see deps, EVEN though
|
||||||
|
# the callpath spec is missing.
|
||||||
|
validate_callpath_spec(False)
|
||||||
|
|
||||||
|
# BUT, make sure that the removed callpath spec is not in queries
|
||||||
|
def db_specs():
|
||||||
|
all_specs = spack.store.layout.all_specs()
|
||||||
|
return (
|
||||||
|
all_specs,
|
||||||
|
[s for s in all_specs if s.satisfies('mpileaks')],
|
||||||
|
[s for s in all_specs if s.satisfies('callpath')],
|
||||||
|
[s for s in all_specs if s.satisfies('mpi')]
|
||||||
|
)
|
||||||
|
all_specs, mpileaks_specs, callpath_specs, mpi_specs = db_specs()
|
||||||
|
assert len(all_specs) == 13
|
||||||
|
assert len(mpileaks_specs) == 3
|
||||||
|
assert len(callpath_specs) == 2
|
||||||
|
assert len(mpi_specs) == 3
|
||||||
|
|
||||||
|
# Now, REINSTALL the spec and make sure everything still holds
|
||||||
|
install('--fake', '/%s' % dag_hash[:7])
|
||||||
|
|
||||||
|
validate_callpath_spec(True)
|
||||||
|
|
||||||
|
all_specs, mpileaks_specs, callpath_specs, mpi_specs = db_specs()
|
||||||
|
assert len(all_specs) == 14 # back to 14
|
||||||
|
assert len(mpileaks_specs) == 3
|
||||||
|
assert len(callpath_specs) == 3 # back to 3
|
||||||
|
assert len(mpi_specs) == 3
|
||||||
|
Loading…
Reference in New Issue
Block a user