Source code for spack.test.patch

# Copyright 2013-2022 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, working_dir

import spack.patch
import spack.paths
import spack.repo
import spack.util.compression
from spack.spec import Spec
from spack.stage import Stage
from spack.util.executable import Executable
from spack.util.path import is_windows

# 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 not is_windows else 'bf874c7dd3a83cf370fdc17e496e341de06cd596b5c66dbf3c9bb7f6c139e3ee'
bar_sha256 = '7d865e959b2466918c9863afca942d0fb89d7c9ac0c99bafc3749504ded97730' if not is_windows else '556ddc69a75d0be0ecafc82cd4657666c8063f13d762282059c39ff5dbf18116'
baz_sha256 = 'bf07a7fbb825fc0aae7bf4a1177b2b31fcf8a3feeaf7092761e18c859ee52a9c' if not is_windows else 'd30392e66c636a063769cbb1db08cd3455a424650d4494db6379d73ea799582b'
biz_sha256 = 'a69b288d7393261e613c276c6d38a01461028291f6e381623acc58139d01f54d' if not is_windows 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 not is_windows else 'ecf44a8244a486e9ef5f72c6cb622f99718dcd790707ac91af0b8c9a4ab7a2bb'


[docs]@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')
[docs]@pytest.mark.skipif(sys.platform == 'win32', reason="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): # Make a patch object url = 'file://' + filename pkg = spack.repo.get('patch') patch = spack.patch.UrlPatch( pkg, 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 patch.fetch() patch.apply(stage) patch.clean() with working_dir(stage.source_path): assert filecmp.cmp('foo.txt', 'foo-expected.txt')
[docs]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))
[docs]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
[docs]def test_patch_order(mock_packages, config): spec = Spec('dep-diamond-patch-top') spec.concretize() mid2_sha256 = 'mid21234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234' \ if not is_windows \ else 'mid21234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234' mid1_sha256 = '0b62284961dab49887e31319843431ee5b037382ac02c4fe436955abef11f094' \ if not is_windows else 'aeb16c4dec1087e39f2330542d59d9b456dd26d791338ae6d80b6ffd10c89dfa' top_sha256 = 'f7de2947c64cb6435e15fb2bef359d1ed5f6356b2aebb7b20535e3772904e6db' \ if not is_windows 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)
[docs]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
[docs]@pytest.mark.skipif(sys.platform == 'win32', reason="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 not is_windows 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()
[docs]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)
[docs]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)
[docs]def check_multi_dependency_patch_specs( libelf, libdwarf, fake, # specs owner, package_dir): # 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
[docs]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)
[docs]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)
[docs]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('')
[docs]@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, '')