summaryrefslogtreecommitdiff
path: root/lib/spack/spack/test/patch.py
blob: bad8f2403daa5196e23f5fb1b962d3137c2315be (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
# 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 collections
import filecmp
import os
import sys

import pytest

from llnl.util.filesystem import mkdirp, touch, working_dir

import spack.patch
import spack.paths
import spack.repo
import spack.util.compression
import spack.util.url as url_util
from spack.spec import Spec
from spack.stage import Stage
from spack.util.executable import Executable

# various sha256 sums (using variables for legibility)
# many file based shas will differ between Windows and other platforms
# due to the use of carriage returns ('\r\n') in Windows line endings

# files with contents 'foo', 'bar', and 'baz'
foo_sha256 = (
    "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c"
    if sys.platform != "win32"
    else "bf874c7dd3a83cf370fdc17e496e341de06cd596b5c66dbf3c9bb7f6c139e3ee"
)
bar_sha256 = (
    "7d865e959b2466918c9863afca942d0fb89d7c9ac0c99bafc3749504ded97730"
    if sys.platform != "win32"
    else "556ddc69a75d0be0ecafc82cd4657666c8063f13d762282059c39ff5dbf18116"
)
baz_sha256 = (
    "bf07a7fbb825fc0aae7bf4a1177b2b31fcf8a3feeaf7092761e18c859ee52a9c"
    if sys.platform != "win32"
    else "d30392e66c636a063769cbb1db08cd3455a424650d4494db6379d73ea799582b"
)
biz_sha256 = (
    "a69b288d7393261e613c276c6d38a01461028291f6e381623acc58139d01f54d"
    if sys.platform != "win32"
    else "2f2b087a8f84834fd03d4d1d5b43584011e869e4657504ef3f8b0a672a5c222e"
)

# url patches
# url shas are the same on Windows
url1_sha256 = "abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234"
url2_sha256 = "1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd"
url2_archive_sha256 = "abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd"

platform_url_sha = (
    "252c0af58be3d90e5dc5e0d16658434c9efa5d20a5df6c10bf72c2d77f780866"
    if sys.platform != "win32"
    else "ecf44a8244a486e9ef5f72c6cb622f99718dcd790707ac91af0b8c9a4ab7a2bb"
)


@pytest.fixture()
def mock_patch_stage(tmpdir_factory, monkeypatch):
    # Don't disrupt the spack install directory with tests.
    mock_path = str(tmpdir_factory.mktemp("mock-patch-stage"))
    monkeypatch.setattr(spack.stage, "_stage_root", mock_path)
    return mock_path


data_path = os.path.join(spack.paths.test_path, "data", "patch")


@pytest.mark.not_on_windows("Line ending conflict on Windows")
@pytest.mark.parametrize(
    "filename, sha256, archive_sha256",
    [
        # compressed patch -- needs sha256 and archive_256
        (
            os.path.join(data_path, "foo.tgz"),
            "252c0af58be3d90e5dc5e0d16658434c9efa5d20a5df6c10bf72c2d77f780866",
            "4e8092a161ec6c3a1b5253176fcf33ce7ba23ee2ff27c75dbced589dabacd06e",
        ),
        # uncompressed patch -- needs only sha256
        (os.path.join(data_path, "foo.patch"), platform_url_sha, None),
    ],
)
def test_url_patch(mock_patch_stage, filename, sha256, archive_sha256, config):
    # Make a patch object
    url = url_util.path_to_file_url(filename)
    s = Spec("patch").concretized()
    patch = spack.patch.UrlPatch(s.package, url, sha256=sha256, archive_sha256=archive_sha256)

    # make a stage
    with Stage(url) as stage:  # TODO: url isn't used; maybe refactor Stage
        stage.mirror_path = mock_patch_stage

        mkdirp(stage.source_path)
        with working_dir(stage.source_path):
            # write a file to be patched
            with open("foo.txt", "w") as f:
                f.write(
                    """\
first line
second line
"""
                )
            # write the expected result of patching.
            with open("foo-expected.txt", "w") as f:
                f.write(
                    """\
zeroth line
first line
third line
"""
                )
        # apply the patch and compare files
        with patch.stage:
            patch.stage.create()
            patch.stage.fetch()
            patch.stage.expand_archive()
            patch.apply(stage)

        with working_dir(stage.source_path):
            assert filecmp.cmp("foo.txt", "foo-expected.txt")


def test_patch_in_spec(mock_packages, config):
    """Test whether patches in a package appear in the spec."""
    spec = Spec("patch")
    spec.concretize()
    assert "patches" in list(spec.variants.keys())

    # Here the order is bar, foo, baz. Note that MV variants order
    # lexicographically based on the hash, not on the position of the
    # patch directive.
    assert (bar_sha256, foo_sha256, baz_sha256) == spec.variants["patches"].value

    assert (foo_sha256, bar_sha256, baz_sha256) == tuple(
        spec.variants["patches"]._patches_in_order_of_appearance
    )


def test_patch_mixed_versions_subset_constraint(mock_packages, config):
    """If we have a package with mixed x.y and x.y.z versions, make sure that
    a patch applied to a version range of x.y.z versions is not applied to
    an x.y version.
    """
    spec1 = Spec("patch@1.0.1")
    spec1.concretize()
    assert biz_sha256 in spec1.variants["patches"].value

    spec2 = Spec("patch@=1.0")
    spec2.concretize()
    assert biz_sha256 not in spec2.variants["patches"].value


def test_patch_order(mock_packages, config):
    spec = Spec("dep-diamond-patch-top")
    spec.concretize()

    mid2_sha256 = (
        "mid21234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234"
        if sys.platform != "win32"
        else "mid21234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234"
    )
    mid1_sha256 = (
        "0b62284961dab49887e31319843431ee5b037382ac02c4fe436955abef11f094"
        if sys.platform != "win32"
        else "aeb16c4dec1087e39f2330542d59d9b456dd26d791338ae6d80b6ffd10c89dfa"
    )
    top_sha256 = (
        "f7de2947c64cb6435e15fb2bef359d1ed5f6356b2aebb7b20535e3772904e6db"
        if sys.platform != "win32"
        else "ff34cb21271d16dbf928374f610bb5dd593d293d311036ddae86c4846ff79070"
    )

    dep = spec["patch"]
    patch_order = dep.variants["patches"]._patches_in_order_of_appearance
    # 'mid2' comes after 'mid1' alphabetically
    # 'top' comes after 'mid1'/'mid2' alphabetically
    # 'patch' comes last of all specs in the dag, alphabetically, so the
    # patches of 'patch' to itself are applied last. The patches applied by
    # 'patch' are ordered based on their appearance in the package.py file
    expected_order = (mid1_sha256, mid2_sha256, top_sha256, foo_sha256, bar_sha256, baz_sha256)

    assert expected_order == tuple(patch_order)


def test_nested_directives(mock_packages):
    """Ensure pkg data structures are set up properly by nested directives."""
    # this ensures that the patch() directive results were removed
    # properly from the DirectiveMeta._directives_to_be_executed list
    patcher = spack.repo.PATH.get_pkg_class("patch-several-dependencies")
    assert len(patcher.patches) == 0

    # this ensures that results of dependency patches were properly added
    # to Dependency objects.
    libelf_dep = next(iter(patcher.dependencies["libelf"].values()))
    assert len(libelf_dep.patches) == 1
    assert len(libelf_dep.patches[Spec()]) == 1

    libdwarf_dep = next(iter(patcher.dependencies["libdwarf"].values()))
    assert len(libdwarf_dep.patches) == 2
    assert len(libdwarf_dep.patches[Spec()]) == 1
    assert len(libdwarf_dep.patches[Spec("@20111030")]) == 1

    fake_dep = next(iter(patcher.dependencies["fake"].values()))
    assert len(fake_dep.patches) == 1
    assert len(fake_dep.patches[Spec()]) == 2


@pytest.mark.not_on_windows("Test requires Autotools")
def test_patched_dependency(mock_packages, config, install_mockery, mock_fetch):
    """Test whether patched dependencies work."""
    spec = Spec("patch-a-dependency")
    spec.concretize()
    assert "patches" in list(spec["libelf"].variants.keys())

    # make sure the patch makes it into the dependency spec
    t_sha = (
        "c45c1564f70def3fc1a6e22139f62cb21cd190cc3a7dbe6f4120fa59ce33dcb8"
        if sys.platform != "win32"
        else "3c5b65abcd6a3b2c714dbf7c31ff65fe3748a1adc371f030c283007ca5534f11"
    )
    assert (t_sha,) == spec["libelf"].variants["patches"].value

    # make sure the patch in the dependent's directory is applied to the
    # dependency
    libelf = spec["libelf"]
    pkg = libelf.package
    pkg.do_patch()
    with pkg.stage:
        with working_dir(pkg.stage.source_path):
            # output a Makefile with 'echo Patched!' as the default target
            configure = Executable("./configure")
            configure()

            # Make sure the Makefile contains the patched text
            with open("Makefile") as mf:
                assert "Patched!" in mf.read()


def trigger_bad_patch(pkg):
    if not os.path.isdir(pkg.stage.source_path):
        os.makedirs(pkg.stage.source_path)
    bad_file = os.path.join(pkg.stage.source_path, ".spack_patch_failed")
    touch(bad_file)
    return bad_file


def test_patch_failure_develop_spec_exits_gracefully(
    mock_packages, config, install_mockery, mock_fetch, tmpdir
):
    """
    ensure that a failing patch does not trigger exceptions
    for develop specs
    """

    spec = Spec("patch-a-dependency " "^libelf dev_path=%s" % str(tmpdir))
    spec.concretize()
    libelf = spec["libelf"]
    assert "patches" in list(libelf.variants.keys())
    pkg = libelf.package
    with pkg.stage:
        bad_patch_indicator = trigger_bad_patch(pkg)
        assert os.path.isfile(bad_patch_indicator)
        pkg.do_patch()
    # success if no exceptions raised


def test_patch_failure_restages(mock_packages, config, install_mockery, mock_fetch):
    """
    ensure that a failing patch does not trigger exceptions
    for non-develop specs and the source gets restaged
    """
    spec = Spec("patch-a-dependency")
    spec.concretize()
    pkg = spec["libelf"].package
    with pkg.stage:
        bad_patch_indicator = trigger_bad_patch(pkg)
        assert os.path.isfile(bad_patch_indicator)
        pkg.do_patch()
        assert not os.path.isfile(bad_patch_indicator)


def test_multiple_patched_dependencies(mock_packages, config):
    """Test whether multiple patched dependencies work."""
    spec = Spec("patch-several-dependencies")
    spec.concretize()

    # basic patch on libelf
    assert "patches" in list(spec["libelf"].variants.keys())
    # foo
    assert (foo_sha256,) == spec["libelf"].variants["patches"].value

    # URL patches
    assert "patches" in list(spec["fake"].variants.keys())
    # urlpatch.patch, urlpatch.patch.gz
    assert (url2_sha256, url1_sha256) == spec["fake"].variants["patches"].value


def test_conditional_patched_dependencies(mock_packages, config):
    """Test whether conditional patched dependencies work."""
    spec = Spec("patch-several-dependencies @1.0")
    spec.concretize()

    # basic patch on libelf
    assert "patches" in list(spec["libelf"].variants.keys())
    # foo
    assert (foo_sha256,) == spec["libelf"].variants["patches"].value

    # conditional patch on libdwarf
    assert "patches" in list(spec["libdwarf"].variants.keys())
    # bar
    assert (bar_sha256,) == spec["libdwarf"].variants["patches"].value
    # baz is conditional on libdwarf version
    assert baz_sha256 not in spec["libdwarf"].variants["patches"].value

    # URL patches
    assert "patches" in list(spec["fake"].variants.keys())
    # urlpatch.patch, urlpatch.patch.gz
    assert (url2_sha256, url1_sha256) == spec["fake"].variants["patches"].value


def check_multi_dependency_patch_specs(
    libelf, libdwarf, fake, owner, package_dir  # specs
):  # parent spec properties
    """Validate patches on dependencies of patch-several-dependencies."""
    # basic patch on libelf
    assert "patches" in list(libelf.variants.keys())
    # foo
    assert foo_sha256 in libelf.variants["patches"].value

    # conditional patch on libdwarf
    assert "patches" in list(libdwarf.variants.keys())
    # bar
    assert bar_sha256 in libdwarf.variants["patches"].value
    # baz is conditional on libdwarf version (no guarantee on order w/conds)
    assert baz_sha256 in libdwarf.variants["patches"].value

    def get_patch(spec, ending):
        return next(p for p in spec.patches if p.path_or_url.endswith(ending))

    # make sure file patches are reconstructed properly
    foo_patch = get_patch(libelf, "foo.patch")
    bar_patch = get_patch(libdwarf, "bar.patch")
    baz_patch = get_patch(libdwarf, "baz.patch")

    assert foo_patch.owner == owner
    assert foo_patch.path == os.path.join(package_dir, "foo.patch")
    assert foo_patch.sha256 == foo_sha256

    assert bar_patch.owner == "builtin.mock.patch-several-dependencies"
    assert bar_patch.path == os.path.join(package_dir, "bar.patch")
    assert bar_patch.sha256 == bar_sha256

    assert baz_patch.owner == "builtin.mock.patch-several-dependencies"
    assert baz_patch.path == os.path.join(package_dir, "baz.patch")
    assert baz_patch.sha256 == baz_sha256

    # URL patches
    assert "patches" in list(fake.variants.keys())
    # urlpatch.patch, urlpatch.patch.gz
    assert (url2_sha256, url1_sha256) == fake.variants["patches"].value

    url1_patch = get_patch(fake, "urlpatch.patch")
    url2_patch = get_patch(fake, "urlpatch2.patch.gz")

    assert url1_patch.owner == "builtin.mock.patch-several-dependencies"
    assert url1_patch.url == "http://example.com/urlpatch.patch"
    assert url1_patch.sha256 == url1_sha256

    assert url2_patch.owner == "builtin.mock.patch-several-dependencies"
    assert url2_patch.url == "http://example.com/urlpatch2.patch.gz"
    assert url2_patch.sha256 == url2_sha256
    assert url2_patch.archive_sha256 == url2_archive_sha256


def test_conditional_patched_deps_with_conditions(mock_packages, config):
    """Test whether conditional patched dependencies with conditions work."""
    spec = Spec("patch-several-dependencies @1.0 ^libdwarf@20111030")
    spec.concretize()

    libelf = spec["libelf"]
    libdwarf = spec["libdwarf"]
    fake = spec["fake"]

    check_multi_dependency_patch_specs(
        libelf, libdwarf, fake, "builtin.mock.patch-several-dependencies", spec.package.package_dir
    )


def test_write_and_read_sub_dags_with_patched_deps(mock_packages, config):
    """Test whether patched dependencies are still correct after writing and
    reading a sub-DAG of a concretized Spec.
    """
    spec = Spec("patch-several-dependencies @1.0 ^libdwarf@20111030")
    spec.concretize()

    # write to YAML and read back in -- new specs will *only* contain
    # their sub-DAGs, and won't contain the dependent that patched them
    libelf = spack.spec.Spec.from_yaml(spec["libelf"].to_yaml())
    libdwarf = spack.spec.Spec.from_yaml(spec["libdwarf"].to_yaml())
    fake = spack.spec.Spec.from_yaml(spec["fake"].to_yaml())

    # make sure we can still read patches correctly for these specs
    check_multi_dependency_patch_specs(
        libelf, libdwarf, fake, "builtin.mock.patch-several-dependencies", spec.package.package_dir
    )


def test_patch_no_file():
    # Give it the attributes we need to construct the error message
    FakePackage = collections.namedtuple("FakePackage", ["name", "namespace", "fullname"])
    fp = FakePackage("fake-package", "test", "fake-package")
    with pytest.raises(ValueError, match="FilePatch:"):
        spack.patch.FilePatch(fp, "nonexistent_file", 0, "")

    patch = spack.patch.Patch(fp, "nonexistent_file", 0, "")
    patch.path = "test"
    with pytest.raises(spack.patch.NoSuchPatchError, match="No such patch:"):
        patch.apply("")


@pytest.mark.parametrize("level", [-1, 0.0, "1"])
def test_invalid_level(level):
    # Give it the attributes we need to construct the error message
    FakePackage = collections.namedtuple("FakePackage", ["name", "namespace"])
    fp = FakePackage("fake-package", "test")
    with pytest.raises(ValueError, match="Patch level needs to be a non-negative integer."):
        spack.patch.Patch(fp, "nonexistent_file", level, "")