Use Spec.constrain to construct spec lists for stacks (#28783)
* stacks: add regression tests for matrix expansion * Use constrain semantics to construct spec lists for stacks * Fix semantics for constraining an anonymous spec. Add tests
This commit is contained in:
		 Massimiliano Culpo
					Massimiliano Culpo
				
			
				
					committed by
					
						 GitHub
						GitHub
					
				
			
			
				
	
			
			
			 GitHub
						GitHub
					
				
			
						parent
						
							d668dea97d
						
					
				
				
					commit
					5881a03408
				
			| @@ -3170,6 +3170,15 @@ def constrain(self, other, deps=True): | |||||||
|                     raise UnsatisfiableArchitectureSpecError(sarch, oarch) |                     raise UnsatisfiableArchitectureSpecError(sarch, oarch) | ||||||
| 
 | 
 | ||||||
|         changed = False |         changed = False | ||||||
|  | 
 | ||||||
|  |         if not self.name and other.name: | ||||||
|  |             self.name = other.name | ||||||
|  |             changed = True | ||||||
|  | 
 | ||||||
|  |         if not self.namespace and other.namespace: | ||||||
|  |             self.namespace = other.namespace | ||||||
|  |             changed = True | ||||||
|  | 
 | ||||||
|         if self.compiler is not None and other.compiler is not None: |         if self.compiler is not None and other.compiler is not None: | ||||||
|             changed |= self.compiler.constrain(other.compiler) |             changed |= self.compiler.constrain(other.compiler) | ||||||
|         elif self.compiler is None: |         elif self.compiler is None: | ||||||
|   | |||||||
| @@ -11,19 +11,6 @@ | |||||||
| from spack.spec import Spec | from spack.spec import Spec | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def spec_ordering_key(s): |  | ||||||
|     if s.startswith('^'): |  | ||||||
|         return 5 |  | ||||||
|     elif s.startswith('/'): |  | ||||||
|         return 4 |  | ||||||
|     elif s.startswith('%'): |  | ||||||
|         return 3 |  | ||||||
|     elif any(s.startswith(c) for c in '~-+@') or '=' in s: |  | ||||||
|         return 2 |  | ||||||
|     else: |  | ||||||
|         return 1 |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| class SpecList(object): | class SpecList(object): | ||||||
| 
 | 
 | ||||||
|     def __init__(self, name='specs', yaml_list=None, reference=None): |     def __init__(self, name='specs', yaml_list=None, reference=None): | ||||||
| @@ -177,30 +164,36 @@ def __getitem__(self, key): | |||||||
|         return self.specs[key] |         return self.specs[key] | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def _expand_matrix_constraints(object, specify=True): | def _expand_matrix_constraints(matrix_config): | ||||||
|     # recurse so we can handle nexted matrices |     # recurse so we can handle nested matrices | ||||||
|     expanded_rows = [] |     expanded_rows = [] | ||||||
|     for row in object['matrix']: |     for row in matrix_config['matrix']: | ||||||
|         new_row = [] |         new_row = [] | ||||||
|         for r in row: |         for r in row: | ||||||
|             if isinstance(r, dict): |             if isinstance(r, dict): | ||||||
|  |                 # Flatten the nested matrix into a single row of constraints | ||||||
|                 new_row.extend( |                 new_row.extend( | ||||||
|                     [[' '.join(c)] |                     [[' '.join([str(c) for c in expanded_constraint_list])] | ||||||
|                      for c in _expand_matrix_constraints(r, specify=False)]) |                      for expanded_constraint_list in _expand_matrix_constraints(r)] | ||||||
|  |                 ) | ||||||
|             else: |             else: | ||||||
|                 new_row.append([r]) |                 new_row.append([r]) | ||||||
|         expanded_rows.append(new_row) |         expanded_rows.append(new_row) | ||||||
| 
 | 
 | ||||||
|     excludes = object.get('exclude', [])  # only compute once |     excludes = matrix_config.get('exclude', [])  # only compute once | ||||||
|     sigil = object.get('sigil', '') |     sigil = matrix_config.get('sigil', '') | ||||||
| 
 | 
 | ||||||
|     results = [] |     results = [] | ||||||
|     for combo in itertools.product(*expanded_rows): |     for combo in itertools.product(*expanded_rows): | ||||||
|         # Construct a combined spec to test against excludes |         # Construct a combined spec to test against excludes | ||||||
|         flat_combo = [constraint for list in combo for constraint in list] |         flat_combo = [constraint for constraint_list in combo | ||||||
|         ordered_combo = sorted(flat_combo, key=spec_ordering_key) |                       for constraint in constraint_list] | ||||||
|  |         flat_combo = [Spec(x) for x in flat_combo] | ||||||
|  | 
 | ||||||
|  |         test_spec = flat_combo[0].copy() | ||||||
|  |         for constraint in flat_combo[1:]: | ||||||
|  |             test_spec.constrain(constraint) | ||||||
| 
 | 
 | ||||||
|         test_spec = Spec(' '.join(ordered_combo)) |  | ||||||
|         # Abstract variants don't have normal satisfaction semantics |         # Abstract variants don't have normal satisfaction semantics | ||||||
|         # Convert all variants to concrete types. |         # Convert all variants to concrete types. | ||||||
|         # This method is best effort, so all existing variants will be |         # This method is best effort, so all existing variants will be | ||||||
| @@ -214,14 +207,12 @@ def _expand_matrix_constraints(object, specify=True): | |||||||
|         if any(test_spec.satisfies(x) for x in excludes): |         if any(test_spec.satisfies(x) for x in excludes): | ||||||
|             continue |             continue | ||||||
| 
 | 
 | ||||||
|         if sigil:  # add sigil if necessary |         if sigil: | ||||||
|             ordered_combo[0] = sigil + ordered_combo[0] |             flat_combo[0] = Spec(sigil + str(flat_combo[0])) | ||||||
| 
 | 
 | ||||||
|         # Add to list of constraints |         # Add to list of constraints | ||||||
|         if specify: |         results.append(flat_combo) | ||||||
|             results.append([Spec(x) for x in ordered_combo]) | 
 | ||||||
|         else: |  | ||||||
|             results.append(ordered_combo) |  | ||||||
|     return results |     return results | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|   | |||||||
| @@ -45,24 +45,34 @@ def test_spec_list_expansions(self): | |||||||
|         assert speclist.specs_as_constraints == self.default_constraints |         assert speclist.specs_as_constraints == self.default_constraints | ||||||
|         assert speclist.specs == self.default_specs |         assert speclist.specs == self.default_specs | ||||||
| 
 | 
 | ||||||
|     def test_spec_list_constraint_ordering(self): |     @pytest.mark.regression('28749') | ||||||
|         specs = [{'matrix': [ |     @pytest.mark.parametrize('specs,expected', [ | ||||||
|  |         # Constraints are ordered randomly | ||||||
|  |         ([{'matrix': [ | ||||||
|             ['^zmpi'], |             ['^zmpi'], | ||||||
|             ['%gcc@4.5.0'], |             ['%gcc@4.5.0'], | ||||||
|             ['hypre', 'libelf'], |             ['hypre', 'libelf'], | ||||||
|             ['~shared'], |             ['~shared'], | ||||||
|             ['cflags=-O3', 'cflags="-g -O0"'], |             ['cflags=-O3', 'cflags="-g -O0"'], | ||||||
|             ['^foo'] |             ['^foo'] | ||||||
|         ]}] |         ]}], [ | ||||||
| 
 |             'hypre cflags=-O3 ~shared %gcc@4.5.0 ^foo ^zmpi', | ||||||
|  |             'hypre cflags="-g -O0" ~shared %gcc@4.5.0 ^foo ^zmpi', | ||||||
|  |             'libelf cflags=-O3 ~shared %gcc@4.5.0 ^foo ^zmpi', | ||||||
|  |             'libelf cflags="-g -O0" ~shared %gcc@4.5.0 ^foo ^zmpi', | ||||||
|  |         ]), | ||||||
|  |         # A constraint affects both the root and a dependency | ||||||
|  |         ([{'matrix': [ | ||||||
|  |             ['gromacs'], | ||||||
|  |             ['%gcc'], | ||||||
|  |             ['+plumed ^plumed%gcc'] | ||||||
|  |         ]}], [ | ||||||
|  |             'gromacs+plumed%gcc ^plumed%gcc' | ||||||
|  |         ]) | ||||||
|  |     ]) | ||||||
|  |     def test_spec_list_constraint_ordering(self, specs, expected): | ||||||
|         speclist = SpecList('specs', specs) |         speclist = SpecList('specs', specs) | ||||||
| 
 |         expected_specs = [Spec(x) for x in expected] | ||||||
|         expected_specs = [ |  | ||||||
|             Spec('hypre cflags=-O3 ~shared %gcc@4.5.0 ^foo ^zmpi'), |  | ||||||
|             Spec('hypre cflags="-g -O0" ~shared %gcc@4.5.0 ^foo ^zmpi'), |  | ||||||
|             Spec('libelf cflags=-O3 ~shared %gcc@4.5.0 ^foo ^zmpi'), |  | ||||||
|             Spec('libelf cflags="-g -O0" ~shared %gcc@4.5.0 ^foo ^zmpi'), |  | ||||||
|         ] |  | ||||||
|         assert speclist.specs == expected_specs |         assert speclist.specs == expected_specs | ||||||
| 
 | 
 | ||||||
|     def test_spec_list_add(self): |     def test_spec_list_add(self): | ||||||
|   | |||||||
| @@ -1228,3 +1228,15 @@ def test_merge_abstract_anonymous_specs(specs, expected): | |||||||
|     specs = [Spec(x) for x in specs] |     specs = [Spec(x) for x in specs] | ||||||
|     result = spack.spec.merge_abstract_anonymous_specs(*specs) |     result = spack.spec.merge_abstract_anonymous_specs(*specs) | ||||||
|     assert result == Spec(expected) |     assert result == Spec(expected) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @pytest.mark.parametrize('anonymous,named,expected', [ | ||||||
|  |     ('+plumed', 'gromacs', 'gromacs+plumed'), | ||||||
|  |     ('+plumed ^plumed%gcc', 'gromacs', 'gromacs+plumed ^plumed%gcc'), | ||||||
|  |     ('+plumed', 'builtin.gromacs', 'builtin.gromacs+plumed') | ||||||
|  | ]) | ||||||
|  | def test_merge_anonymous_spec_with_named_spec(anonymous, named, expected): | ||||||
|  |     s = Spec(anonymous) | ||||||
|  |     changed = s.constrain(named) | ||||||
|  |     assert changed | ||||||
|  |     assert s == Spec(expected) | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user