summaryrefslogtreecommitdiff
path: root/lib/spack/spack/test/concretize_preferences.py
blob: 74549599afbdf3f6887d2eebc08e3dbc09c40c1f (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
# Copyright 2013-2024 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)

import os
import stat

import pytest

import spack.config
import spack.package_prefs
import spack.repo
import spack.util.spack_yaml as syaml
from spack.config import ConfigError
from spack.spec import CompilerSpec, Spec
from spack.version import Version


@pytest.fixture()
def configure_permissions():
    conf = syaml.load_config(
        """\
all:
  permissions:
    read: group
    write: group
    group: all
mpich:
  permissions:
    read: user
    write: user
mpileaks:
  permissions:
    write: user
    group: mpileaks
callpath:
  permissions:
    write: world
"""
    )
    spack.config.set("packages", conf, scope="concretize")

    yield


def concretize(abstract_spec):
    return Spec(abstract_spec).concretized()


def update_packages(pkgname, section, value):
    """Update config and reread package list"""
    conf = {pkgname: {section: value}}
    spack.config.set("packages", conf, scope="concretize")


def assert_variant_values(spec, **variants):
    concrete = concretize(spec)
    for variant, value in variants.items():
        assert concrete.variants[variant].value == value


@pytest.mark.usefixtures("concretize_scope", "mock_packages")
class TestConcretizePreferences:
    @pytest.mark.parametrize(
        "package_name,variant_value,expected_results",
        [
            (
                "mpileaks",
                "~debug~opt+shared+static",
                {"debug": False, "opt": False, "shared": True, "static": True},
            ),
            # Check that using a list of variants instead of a single string works
            (
                "mpileaks",
                ["~debug", "~opt", "+shared", "+static"],
                {"debug": False, "opt": False, "shared": True, "static": True},
            ),
            # Use different values for the variants and check them again
            (
                "mpileaks",
                ["+debug", "+opt", "~shared", "-static"],
                {"debug": True, "opt": True, "shared": False, "static": False},
            ),
            # Check a multivalued variant with multiple values set
            (
                "multivalue-variant",
                ["foo=bar,baz", "fee=bar"],
                {"foo": ("bar", "baz"), "fee": "bar"},
            ),
            ("singlevalue-variant", ["fum=why"], {"fum": "why"}),
        ],
    )
    def test_preferred_variants(self, package_name, variant_value, expected_results):
        """Test preferred variants are applied correctly"""
        update_packages(package_name, "variants", variant_value)
        assert_variant_values(package_name, **expected_results)

    def test_preferred_variants_from_wildcard(self):
        """
        Test that 'foo=*' concretizes to any value
        """
        update_packages("multivalue-variant", "variants", "foo=bar")
        assert_variant_values("multivalue-variant foo=*", foo=("bar",))

    @pytest.mark.parametrize(
        "compiler_str,spec_str",
        [("gcc@=4.5.0", "mpileaks"), ("clang@=12.0.0", "mpileaks"), ("gcc@=4.5.0", "openmpi")],
    )
    def test_preferred_compilers(self, compiler_str, spec_str):
        """Test preferred compilers are applied correctly"""
        update_packages("all", "compiler", [compiler_str])
        spec = spack.spec.Spec(spec_str).concretized()
        assert spec.compiler == CompilerSpec(compiler_str)

    @pytest.mark.only_clingo("Use case not supported by the original concretizer")
    def test_preferred_target(self, mutable_mock_repo):
        """Test preferred targets are applied correctly"""
        spec = concretize("mpich")
        default = str(spec.target)
        preferred = str(spec.target.family)

        update_packages("all", "target", [preferred])
        spec = concretize("mpich")
        assert str(spec.target) == preferred

        spec = concretize("mpileaks")
        assert str(spec["mpileaks"].target) == preferred
        assert str(spec["mpich"].target) == preferred

        update_packages("all", "target", [default])
        spec = concretize("mpileaks")
        assert str(spec["mpileaks"].target) == default
        assert str(spec["mpich"].target) == default

    def test_preferred_versions(self):
        """Test preferred package versions are applied correctly"""
        update_packages("mpileaks", "version", ["2.3"])
        spec = concretize("mpileaks")
        assert spec.version == Version("2.3")

        update_packages("mpileaks", "version", ["2.2"])
        spec = concretize("mpileaks")
        assert spec.version == Version("2.2")

    @pytest.mark.only_clingo("This behavior is not enforced for the old concretizer")
    def test_preferred_versions_mixed_version_types(self):
        update_packages("mixedversions", "version", ["=2.0"])
        spec = concretize("mixedversions")
        assert spec.version == Version("2.0")

    def test_preferred_providers(self):
        """Test preferred providers of virtual packages are
        applied correctly
        """
        update_packages("all", "providers", {"mpi": ["mpich"]})
        spec = concretize("mpileaks")
        assert "mpich" in spec

        update_packages("all", "providers", {"mpi": ["zmpi"]})
        spec = concretize("mpileaks")
        assert "zmpi" in spec

    def test_config_set_pkg_property_url(self, mutable_mock_repo):
        """Test setting an existing attribute in the package class"""
        update_packages(
            "mpileaks",
            "package_attributes",
            {"url": "http://www.somewhereelse.com/mpileaks-1.0.tar.gz"},
        )
        spec = concretize("mpileaks")
        assert spec.package.fetcher.url == "http://www.somewhereelse.com/mpileaks-2.3.tar.gz"

        update_packages("mpileaks", "package_attributes", {})
        spec = concretize("mpileaks")
        assert spec.package.fetcher.url == "http://www.llnl.gov/mpileaks-2.3.tar.gz"

    def test_config_set_pkg_property_new(self, mutable_mock_repo):
        """Test that you can set arbitrary attributes on the Package class"""
        conf = syaml.load_config(
            """\
mpileaks:
  package_attributes:
    v1: 1
    v2: true
    v3: yesterday
    v4: "true"
    v5:
      x: 1
      y: 2
    v6:
    - 1
    - 2
"""
        )
        spack.config.set("packages", conf, scope="concretize")

        spec = concretize("mpileaks")
        assert spec.package.v1 == 1
        assert spec.package.v2 is True
        assert spec.package.v3 == "yesterday"
        assert spec.package.v4 == "true"
        assert dict(spec.package.v5) == {"x": 1, "y": 2}
        assert list(spec.package.v6) == [1, 2]

        update_packages("mpileaks", "package_attributes", {})
        spec = concretize("mpileaks")
        with pytest.raises(AttributeError):
            spec.package.v1

    def test_preferred(self):
        """ "Test packages with some version marked as preferred=True"""
        spec = Spec("python")
        spec.concretize()
        assert spec.version == Version("2.7.11")

        # now add packages.yaml with versions other than preferred
        # ensure that once config is in place, non-preferred version is used
        update_packages("python", "version", ["3.5.0"])
        spec = Spec("python")
        spec.concretize()
        assert spec.version == Version("3.5.0")

    @pytest.mark.only_clingo("This behavior is not enforced for the old concretizer")
    def test_preferred_undefined_raises(self):
        """Preference should not specify an undefined version"""
        update_packages("python", "version", ["3.5.0.1"])
        spec = Spec("python")
        with pytest.raises(spack.config.ConfigError):
            spec.concretize()

    @pytest.mark.only_clingo("This behavior is not enforced for the old concretizer")
    def test_preferred_truncated(self):
        """Versions without "=" are treated as version ranges: if there is
        a satisfying version defined in the package.py, we should use that
        (don't define a new version).
        """
        update_packages("python", "version", ["3.5"])
        spec = Spec("python")
        spec.concretize()
        assert spec.satisfies("@3.5.1")

    def test_develop(self):
        """Test concretization with develop-like versions"""
        spec = Spec("develop-test")
        spec.concretize()
        assert spec.version == Version("0.2.15")
        spec = Spec("develop-test2")
        spec.concretize()
        assert spec.version == Version("0.2.15")

        # now add packages.yaml with develop-like versions
        # ensure that once config is in place, develop-like version is used
        update_packages("develop-test", "version", ["develop"])
        spec = Spec("develop-test")
        spec.concretize()
        assert spec.version == Version("develop")

        update_packages("develop-test2", "version", ["0.2.15.develop"])
        spec = Spec("develop-test2")
        spec.concretize()
        assert spec.version == Version("0.2.15.develop")

    def test_external_mpi(self):
        # make sure this doesn't give us an external first.
        spec = Spec("mpi")
        spec.concretize()
        assert not spec["mpi"].external

        # load config
        conf = syaml.load_config(
            """\
all:
    providers:
        mpi: [mpich]
mpich:
    buildable: false
    externals:
    - spec: mpich@3.0.4
      prefix: /dummy/path
"""
        )
        spack.config.set("packages", conf, scope="concretize")

        # ensure that once config is in place, external is used
        spec = Spec("mpi")
        spec.concretize()
        assert spec["mpich"].external_path == os.path.sep + os.path.join("dummy", "path")

    def test_external_module(self, monkeypatch):
        """Test that packages can find externals specified by module

        The specific code for parsing the module is tested elsewhere.
        This just tests that the preference is accounted for"""

        # make sure this doesn't give us an external first.
        def mock_module(cmd, module):
            return "prepend-path PATH /dummy/path"

        monkeypatch.setattr(spack.util.module_cmd, "module", mock_module)

        spec = Spec("mpi")
        spec.concretize()
        assert not spec["mpi"].external

        # load config
        conf = syaml.load_config(
            """\
all:
    providers:
        mpi: [mpich]
mpi:
    buildable: false
    externals:
    - spec: mpich@3.0.4
      modules: [dummy]
"""
        )
        spack.config.set("packages", conf, scope="concretize")

        # ensure that once config is in place, external is used
        spec = Spec("mpi")
        spec.concretize()
        assert spec["mpich"].external_path == os.path.sep + os.path.join("dummy", "path")

    def test_buildable_false(self):
        conf = syaml.load_config(
            """\
libelf:
  buildable: false
"""
        )
        spack.config.set("packages", conf, scope="concretize")
        spec = Spec("libelf")
        assert not spack.package_prefs.is_spec_buildable(spec)

        spec = Spec("mpich")
        assert spack.package_prefs.is_spec_buildable(spec)

    def test_buildable_false_virtual(self):
        conf = syaml.load_config(
            """\
mpi:
  buildable: false
"""
        )
        spack.config.set("packages", conf, scope="concretize")
        spec = Spec("libelf")
        assert spack.package_prefs.is_spec_buildable(spec)

        spec = Spec("mpich")
        assert not spack.package_prefs.is_spec_buildable(spec)

    def test_buildable_false_all(self):
        conf = syaml.load_config(
            """\
all:
  buildable: false
"""
        )
        spack.config.set("packages", conf, scope="concretize")
        spec = Spec("libelf")
        assert not spack.package_prefs.is_spec_buildable(spec)

        spec = Spec("mpich")
        assert not spack.package_prefs.is_spec_buildable(spec)

    def test_buildable_false_all_true_package(self):
        conf = syaml.load_config(
            """\
all:
  buildable: false
libelf:
  buildable: true
"""
        )
        spack.config.set("packages", conf, scope="concretize")
        spec = Spec("libelf")
        assert spack.package_prefs.is_spec_buildable(spec)

        spec = Spec("mpich")
        assert not spack.package_prefs.is_spec_buildable(spec)

    def test_buildable_false_all_true_virtual(self):
        conf = syaml.load_config(
            """\
all:
  buildable: false
mpi:
  buildable: true
"""
        )
        spack.config.set("packages", conf, scope="concretize")
        spec = Spec("libelf")
        assert not spack.package_prefs.is_spec_buildable(spec)

        spec = Spec("mpich")
        assert spack.package_prefs.is_spec_buildable(spec)

    def test_buildable_false_virtual_true_pacakge(self):
        conf = syaml.load_config(
            """\
mpi:
  buildable: false
mpich:
  buildable: true
"""
        )
        spack.config.set("packages", conf, scope="concretize")

        spec = Spec("zmpi")
        assert not spack.package_prefs.is_spec_buildable(spec)

        spec = Spec("mpich")
        assert spack.package_prefs.is_spec_buildable(spec)

    def test_config_permissions_from_all(self, configure_permissions):
        # Although these aren't strictly about concretization, they are
        # configured in the same file and therefore convenient to test here.
        # Make sure we can configure readable and writable

        # Test inheriting from 'all'
        spec = Spec("zmpi")
        perms = spack.package_prefs.get_package_permissions(spec)
        assert perms == stat.S_IRWXU | stat.S_IRWXG

        dir_perms = spack.package_prefs.get_package_dir_permissions(spec)
        assert dir_perms == stat.S_IRWXU | stat.S_IRWXG | stat.S_ISGID

        group = spack.package_prefs.get_package_group(spec)
        assert group == "all"

    def test_config_permissions_from_package(self, configure_permissions):
        # Test overriding 'all'
        spec = Spec("mpich")
        perms = spack.package_prefs.get_package_permissions(spec)
        assert perms == stat.S_IRWXU

        dir_perms = spack.package_prefs.get_package_dir_permissions(spec)
        assert dir_perms == stat.S_IRWXU

        group = spack.package_prefs.get_package_group(spec)
        assert group == "all"

    def test_config_permissions_differ_read_write(self, configure_permissions):
        # Test overriding group from 'all' and different readable/writable
        spec = Spec("mpileaks")
        perms = spack.package_prefs.get_package_permissions(spec)
        assert perms == stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP

        dir_perms = spack.package_prefs.get_package_dir_permissions(spec)
        expected = stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP | stat.S_ISGID
        assert dir_perms == expected

        group = spack.package_prefs.get_package_group(spec)
        assert group == "mpileaks"

    def test_config_perms_fail_write_gt_read(self, configure_permissions):
        # Test failure for writable more permissive than readable
        spec = Spec("callpath")
        with pytest.raises(ConfigError):
            spack.package_prefs.get_package_permissions(spec)

    @pytest.mark.regression("20040")
    def test_variant_not_flipped_to_pull_externals(self):
        """Test that a package doesn't prefer pulling in an
        external to using the default value of a variant.
        """
        s = Spec("vdefault-or-external-root").concretized()

        assert "~external" in s["vdefault-or-external"]
        assert "externaltool" not in s

    @pytest.mark.regression("25585")
    def test_dependencies_cant_make_version_parent_score_better(self):
        """Test that a package can't select a worse version for a
        dependent because doing so it can pull-in a dependency
        that makes the overall version score even or better and maybe
        has a better score in some lower priority criteria.
        """
        s = Spec("version-test-root").concretized()

        assert s.satisfies("^version-test-pkg@2.4.6")
        assert "version-test-dependency-preferred" not in s

    @pytest.mark.regression("26598")
    def test_multivalued_variants_are_lower_priority_than_providers(self):
        """Test that the rule to maximize the number of values for multivalued
        variants is considered at lower priority than selecting the default
        provider for virtual dependencies.

        This ensures that we don't e.g. select openmpi over mpich even if we
        specified mpich as the default mpi provider, just because openmpi supports
        more fabrics by default.
        """
        with spack.config.override(
            "packages:all", {"providers": {"somevirtual": ["some-virtual-preferred"]}}
        ):
            s = Spec("somevirtual").concretized()
            assert s.name == "some-virtual-preferred"

    @pytest.mark.regression("26721,19736")
    def test_sticky_variant_accounts_for_packages_yaml(self):
        with spack.config.override("packages:sticky-variant", {"variants": "+allow-gcc"}):
            s = Spec("sticky-variant %gcc").concretized()
            assert s.satisfies("%gcc") and s.satisfies("+allow-gcc")

    @pytest.mark.regression("41134")
    @pytest.mark.only_clingo("Not backporting the fix to the old concretizer")
    def test_default_preference_variant_different_type_does_not_error(self):
        """Tests that a different type for an existing variant in the 'all:' section of
        packages.yaml doesn't fail with an error.
        """
        with spack.config.override("packages:all", {"variants": "+foo"}):
            s = Spec("a").concretized()
            assert s.satisfies("foo=bar")