Alert user to failed concretizations (#42655)
With this change an error message is emitted when the result of concretization is in an inconsistent state.
This commit is contained in:
		| @@ -127,10 +127,7 @@ def _process_result(result, show, required_format, kwargs): | ||||
|         print() | ||||
| 
 | ||||
|     if result.unsolved_specs and "solutions" in show: | ||||
|         tty.msg("Unsolved specs") | ||||
|         for spec in result.unsolved_specs: | ||||
|             print(spec) | ||||
|         print() | ||||
|         tty.msg(asp.Result.format_unsolved(result.unsolved_specs)) | ||||
| 
 | ||||
| 
 | ||||
| def solve(parser, args): | ||||
|   | ||||
| @@ -411,7 +411,7 @@ def raise_if_unsat(self): | ||||
|         """ | ||||
|         Raise an appropriate error if the result is unsatisfiable. | ||||
| 
 | ||||
|         The error is an InternalConcretizerError, and includes the minimized cores | ||||
|         The error is an SolverError, and includes the minimized cores | ||||
|         resulting from the solve, formatted to be human readable. | ||||
|         """ | ||||
|         if self.satisfiable: | ||||
| @@ -422,7 +422,7 @@ def raise_if_unsat(self): | ||||
|             constraints = constraints[0] | ||||
| 
 | ||||
|         conflicts = self.format_minimal_cores() | ||||
|         raise InternalConcretizerError(constraints, conflicts=conflicts) | ||||
|         raise SolverError(constraints, conflicts=conflicts) | ||||
| 
 | ||||
|     @property | ||||
|     def specs(self): | ||||
| @@ -435,7 +435,10 @@ def specs(self): | ||||
| 
 | ||||
|     @property | ||||
|     def unsolved_specs(self): | ||||
|         """List of abstract input specs that were not solved.""" | ||||
|         """List of tuples pairing abstract input specs that were not | ||||
|         solved with their associated candidate spec from the solver | ||||
|         (if the solve completed). | ||||
|         """ | ||||
|         if self._unsolved_specs is None: | ||||
|             self._compute_specs_from_answer_set() | ||||
|         return self._unsolved_specs | ||||
| @@ -449,7 +452,7 @@ def specs_by_input(self): | ||||
|     def _compute_specs_from_answer_set(self): | ||||
|         if not self.satisfiable: | ||||
|             self._concrete_specs = [] | ||||
|             self._unsolved_specs = self.abstract_specs | ||||
|             self._unsolved_specs = list((x, None) for x in self.abstract_specs) | ||||
|             self._concrete_specs_by_input = {} | ||||
|             return | ||||
| 
 | ||||
| @@ -470,7 +473,22 @@ def _compute_specs_from_answer_set(self): | ||||
|                 self._concrete_specs.append(answer[node]) | ||||
|                 self._concrete_specs_by_input[input_spec] = answer[node] | ||||
|             else: | ||||
|                 self._unsolved_specs.append(input_spec) | ||||
|                 self._unsolved_specs.append((input_spec, candidate)) | ||||
| 
 | ||||
|     @staticmethod | ||||
|     def format_unsolved(unsolved_specs): | ||||
|         """Create a message providing info on unsolved user specs and for | ||||
|         each one show the associated candidate spec from the solver (if | ||||
|         there is one). | ||||
|         """ | ||||
|         msg = "Unsatisfied input specs:" | ||||
|         for input_spec, candidate in unsolved_specs: | ||||
|             msg += f"\n\tInput spec: {str(input_spec)}" | ||||
|             if candidate: | ||||
|                 msg += f"\n\tCandidate spec: {str(candidate)}" | ||||
|             else: | ||||
|                 msg += "\n\t(No candidate specs from solver)" | ||||
|         return msg | ||||
| 
 | ||||
| 
 | ||||
| def _normalize_packages_yaml(packages_yaml): | ||||
| @@ -805,6 +823,13 @@ def on_model(model): | ||||
|             print("Statistics:") | ||||
|             pprint.pprint(self.control.statistics) | ||||
| 
 | ||||
|         if result.unsolved_specs and setup.concretize_everything: | ||||
|             unsolved_str = Result.format_unsolved(result.unsolved_specs) | ||||
|             raise InternalConcretizerError( | ||||
|                 "Internal Spack error: the solver completed but produced specs" | ||||
|                 f" that do not satisfy the request.\n\t{unsolved_str}" | ||||
|             ) | ||||
| 
 | ||||
|         return result, timer, self.control.statistics | ||||
| 
 | ||||
| 
 | ||||
| @@ -3429,15 +3454,13 @@ def solve_in_rounds( | ||||
|             if not result.satisfiable or not result.specs: | ||||
|                 break | ||||
| 
 | ||||
|             input_specs = result.unsolved_specs | ||||
|             input_specs = list(x for (x, y) in result.unsolved_specs) | ||||
|             for spec in result.specs: | ||||
|                 reusable_specs.extend(spec.traverse()) | ||||
| 
 | ||||
| 
 | ||||
| class UnsatisfiableSpecError(spack.error.UnsatisfiableSpecError): | ||||
|     """ | ||||
|     Subclass for new constructor signature for new concretizer | ||||
|     """ | ||||
|     """There was an issue with the spec that was requested (i.e. a user error).""" | ||||
| 
 | ||||
|     def __init__(self, msg): | ||||
|         super(spack.error.UnsatisfiableSpecError, self).__init__(msg) | ||||
| @@ -3447,8 +3470,21 @@ def __init__(self, msg): | ||||
| 
 | ||||
| 
 | ||||
| class InternalConcretizerError(spack.error.UnsatisfiableSpecError): | ||||
|     """ | ||||
|     Subclass for new constructor signature for new concretizer | ||||
|     """Errors that indicate a bug in Spack.""" | ||||
| 
 | ||||
|     def __init__(self, msg): | ||||
|         super(spack.error.UnsatisfiableSpecError, self).__init__(msg) | ||||
|         self.provided = None | ||||
|         self.required = None | ||||
|         self.constraint_type = None | ||||
| 
 | ||||
| 
 | ||||
| class SolverError(InternalConcretizerError): | ||||
|     """For cases where the solver is unable to produce a solution. | ||||
| 
 | ||||
|     Such cases are unexpected because we allow for solutions with errors, | ||||
|     so for example user specs that are over-constrained should still | ||||
|     get a solution. | ||||
|     """ | ||||
| 
 | ||||
|     def __init__(self, provided, conflicts): | ||||
| @@ -3461,7 +3497,7 @@ def __init__(self, provided, conflicts): | ||||
|         if conflicts: | ||||
|             msg += ", errors are:" + "".join([f"\n    {conflict}" for conflict in conflicts]) | ||||
| 
 | ||||
|         super(spack.error.UnsatisfiableSpecError, self).__init__(msg) | ||||
|         super().__init__(msg) | ||||
| 
 | ||||
|         self.provided = provided | ||||
| 
 | ||||
|   | ||||
| @@ -2091,7 +2091,12 @@ def to_node_dict(self, hash=ht.dag_hash): | ||||
|             if hasattr(variant, "_patches_in_order_of_appearance"): | ||||
|                 d["patches"] = variant._patches_in_order_of_appearance | ||||
| 
 | ||||
|         if self._concrete and hash.package_hash and self._package_hash: | ||||
|         if ( | ||||
|             self._concrete | ||||
|             and hash.package_hash | ||||
|             and hasattr(self, "_package_hash") | ||||
|             and self._package_hash | ||||
|         ): | ||||
|             # We use the attribute here instead of `self.package_hash()` because this | ||||
|             # should *always* be assignhed at concretization time. We don't want to try | ||||
|             # to compute a package hash for concrete spec where a) the package might not | ||||
|   | ||||
| @@ -341,6 +341,7 @@ def test_different_compilers_get_different_flags(self): | ||||
|         assert set(client.compiler_flags["fflags"]) == set(["-O0", "-g"]) | ||||
|         assert not set(cmake.compiler_flags["fflags"]) | ||||
| 
 | ||||
|     @pytest.mark.xfail(reason="Broken, needs to be fixed") | ||||
|     def test_compiler_flags_from_compiler_and_dependent(self): | ||||
|         client = Spec("cmake-client %clang@12.2.0 platform=test os=fe target=fe cflags==-g") | ||||
|         client.concretize() | ||||
| @@ -2093,7 +2094,25 @@ def test_result_specs_is_not_empty(self, specs): | ||||
|         result, _, _ = solver.driver.solve(setup, specs, reuse=[]) | ||||
| 
 | ||||
|         assert result.specs | ||||
|         assert not result.unsolved_specs | ||||
| 
 | ||||
|     @pytest.mark.regression("38664") | ||||
|     def test_unsolved_specs_raises_error(self, monkeypatch, mock_packages, config): | ||||
|         """Check that the solver raises an exception when input specs are not | ||||
|         satisfied. | ||||
|         """ | ||||
|         specs = [Spec("zlib")] | ||||
|         solver = spack.solver.asp.Solver() | ||||
|         setup = spack.solver.asp.SpackSolverSetup() | ||||
| 
 | ||||
|         simulate_unsolved_property = list((x, None) for x in specs) | ||||
| 
 | ||||
|         monkeypatch.setattr(spack.solver.asp.Result, "unsolved_specs", simulate_unsolved_property) | ||||
| 
 | ||||
|         with pytest.raises( | ||||
|             spack.solver.asp.InternalConcretizerError, | ||||
|             match="the solver completed but produced specs", | ||||
|         ): | ||||
|             solver.driver.solve(setup, specs, reuse=[]) | ||||
| 
 | ||||
|     @pytest.mark.regression("36339") | ||||
|     def test_compiler_match_constraints_when_selected(self): | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Peter Scheibel
					Peter Scheibel