import os import subprocess import platform import re import sys import textwrap import pytest X_Y = f'{sys.version_info[0]}.{sys.version_info[1]}' XY = f'{sys.version_info[0]}{sys.version_info[1]}' # Handy environment variable you can use to run the tests # with modified macros files. Multiple files should be # separated by colon. # You can use * if you escape it from your Shell: # TESTED_FILES='macros.*' pytest -v # Remember that some tests might need more macros files than just # the local ones. You might need to use: # TESTED_FILES='/usr/lib/rpm/macros:/usr/lib/rpm/platform/x86_64-linux/macros:macros.*' TESTED_FILES = os.getenv("TESTED_FILES", None) def rpm_eval(expression, fails=False, **kwargs): cmd = ['rpmbuild'] if TESTED_FILES: cmd += ['--macros', TESTED_FILES] for var, value in kwargs.items(): if value is None: cmd += ['--undefine', var] else: cmd += ['--define', f'{var} {value}'] cmd += ['--eval', expression] cp = subprocess.run(cmd, text=True, env={**os.environ, 'LANG': 'C.utf-8'}, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) if fails: assert cp.returncode != 0, cp.stdout elif fails is not None: assert cp.returncode == 0, cp.stdout return cp.stdout.strip().splitlines() @pytest.fixture(scope="session") def lib(): lib_eval = rpm_eval("%_lib")[0] if lib_eval == "%_lib" and TESTED_FILES: raise ValueError( "%_lib is not resolved to an actual value. " "You may want to include /usr/lib/rpm/platform/x86_64-linux/macros to TESTED_FILES." ) return lib_eval def get_alt_x_y(): """ Some tests require alternate Python version to be installed. In order to allow any Python version (or none at all), this function/fixture exists. You can control the behavior by setting the $ALTERNATE_PYTHON_VERSION environment variable to X.Y (e.g. 3.6) or SKIP. The environment variable must be set. """ env_name = "ALTERNATE_PYTHON_VERSION" alternate_python_version = os.getenv(env_name, "") if alternate_python_version.upper() == "SKIP": pytest.skip(f"${env_name} set to SKIP") if not alternate_python_version: raise ValueError(f"${env_name} must be set, " f"set it to SKIP if you want to skip tests that " f"require alternate Python version.") if not re.match(r"^\d+\.\d+$", alternate_python_version): raise ValueError(f"${env_name} must be X.Y") return alternate_python_version def get_alt_xy(): """ Same as get_alt_x_y() but without a dot """ return get_alt_x_y().replace(".", "") # We don't use decorators, to be able to call the functions directly alt_x_y = pytest.fixture(scope="session")(get_alt_x_y) alt_xy = pytest.fixture(scope="session")(get_alt_xy) # https://fedoraproject.org/wiki/Changes/PythonSafePath def safe_path_flag(x_y): return 'P' if tuple(int(i) for i in x_y.split('.')) >= (3, 11) else '' def shell_stdout(script): return subprocess.check_output(script, env={**os.environ, 'LANG': 'C.utf-8'}, text=True, shell=True).rstrip() @pytest.mark.parametrize('macro', ['%__python3', '%python3']) def test_python3(macro): assert rpm_eval(macro) == ['/usr/bin/python3.12'] @pytest.mark.skip("Overriden in python3.12-rpm-macros") @pytest.mark.parametrize('macro', ['%__python3', '%python3']) @pytest.mark.parametrize('pkgversion', ['3', '3.9', '3.12']) def test_python3_with_pkgversion(macro, pkgversion): assert rpm_eval(macro, python3_pkgversion=pkgversion) == [f'/usr/bin/python{pkgversion}'] @pytest.mark.skip("py_dist_name uses the old canonicalization on RHEL 8") @pytest.mark.parametrize('argument, result', [ ('a', 'a'), ('a-a', 'a-a'), ('a_a', 'a-a'), ('a.a', 'a-a'), ('a---a', 'a-a'), ('a-_-a', 'a-a'), ('a-_-a', 'a-a'), ('a[b]', 'a[b]'), ('Aha[Boom]', 'aha[boom]'), ('a.a[b.b]', 'a-a[b-b]'), ]) def test_pydist_name(argument, result): assert rpm_eval(f'%py_dist_name {argument}') == [result] @pytest.mark.skip("py2_dist uses the old canonicalization on RHEL 8") def test_py2_dist(): assert rpm_eval(f'%py2_dist Aha[Boom] a') == ['python2dist(aha[boom]) python2dist(a)'] @pytest.mark.skip("py3_dist uses the old canonicalization on RHEL 8") def test_py3_dist(): assert rpm_eval(f'%py3_dist Aha[Boom] a') == ['python3dist(aha[boom]) python3dist(a)'] def test_py3_dist_with_python3_pkgversion_redefined(alt_x_y): assert rpm_eval(f'%py3_dist Aha[Boom] a', python3_pkgversion=alt_x_y) == [f'python{alt_x_y}dist(aha[boom]) python{alt_x_y}dist(a)'] @pytest.mark.skip("python_provide is still enabled on RHEL 8") def test_python_provide_python(): assert rpm_eval('%python_provide python-foo') == [] @pytest.mark.skip("python_provide does not work with this test when __python3 is overridden") def test_python_provide_python3(): lines = rpm_eval('%python_provide python3-foo', version='6', release='1.fc66') assert 'Obsoletes: python-foo < 6-1.fc66' in lines assert 'Provides: python-foo = 6-1.fc66' in lines assert f'Provides: python{X_Y}-foo = 6-1.fc66' in lines assert len(lines) == 3 @pytest.mark.skip("python_provide does not work with this test when __python3 is overridden") def test_python_provide_python3_epoched(): lines = rpm_eval('%python_provide python3-foo', epoch='1', version='6', release='1.fc66') assert 'Obsoletes: python-foo < 1:6-1.fc66' in lines assert 'Provides: python-foo = 1:6-1.fc66' in lines assert f'Provides: python{X_Y}-foo = 1:6-1.fc66' in lines assert len(lines) == 3 @pytest.mark.skip("python_provide does not work with this test when __python3 is overridden") def test_python_provide_python3X(): lines = rpm_eval(f'%python_provide python{X_Y}-foo', version='6', release='1.fc66') assert 'Obsoletes: python-foo < 6-1.fc66' in lines assert 'Provides: python-foo = 6-1.fc66' in lines assert 'Provides: python3-foo = 6-1.fc66' in lines assert len(lines) == 3 @pytest.mark.skip("python_provide does not work with this test when __python3 is overridden") def test_python_provide_python3X_epoched(): lines = rpm_eval(f'%python_provide python{X_Y}-foo', epoch='1', version='6', release='1.fc66') assert 'Obsoletes: python-foo < 1:6-1.fc66' in lines assert 'Provides: python-foo = 1:6-1.fc66' in lines assert 'Provides: python3-foo = 1:6-1.fc66' in lines assert len(lines) == 3 @pytest.mark.skip("python_provide does not work with this test when __python3 is overridden") def test_python_provide_doubleuse(): lines = rpm_eval('%{python_provide python3-foo}%{python_provide python3-foo}', version='6', release='1.fc66') assert 'Obsoletes: python-foo < 6-1.fc66' in lines assert 'Provides: python-foo = 6-1.fc66' in lines assert f'Provides: python{X_Y}-foo = 6-1.fc66' in lines assert len(lines) == 6 assert len(set(lines)) == 3 @pytest.mark.parametrize('rhel', [None, 10]) def test_py_provides_python(rhel): lines = rpm_eval('%py_provides python-foo', version='6', release='1.fc66', rhel=rhel) assert 'Provides: python-foo = 6-1.fc66' in lines assert len(lines) == 1 @pytest.mark.parametrize('rhel', [None, 12]) def test_py_provides_whatever(rhel): lines = rpm_eval('%py_provides whatever', version='6', release='1.fc66', rhel=rhel) assert 'Provides: whatever = 6-1.fc66' in lines assert len(lines) == 1 @pytest.mark.skip("py_provides behaves differently for alternative Python stacks") @pytest.mark.parametrize('rhel', [None, 9]) def test_py_provides_python3(rhel): lines = rpm_eval('%py_provides python3-foo', version='6', release='1.fc66', rhel=rhel) assert 'Provides: python3-foo = 6-1.fc66' in lines assert 'Provides: python-foo = 6-1.fc66' in lines assert f'Provides: python{X_Y}-foo = 6-1.fc66' in lines if rhel: assert f'Obsoletes: python{X_Y}-foo < 6-1.fc66' in lines assert len(lines) == 4 else: assert len(lines) == 3 @pytest.mark.skip("py_provides behaves differently for alternative Python stacks") @pytest.mark.parametrize('rhel', [None, 9]) def test_py_provides_python3_with_isa(rhel): lines = rpm_eval('%py_provides python3-foo(x86_64)', version='6', release='1.fc66', rhel=rhel) assert 'Provides: python3-foo(x86_64) = 6-1.fc66' in lines assert 'Provides: python-foo(x86_64) = 6-1.fc66' in lines assert f'Provides: python{X_Y}-foo(x86_64) = 6-1.fc66' in lines assert f'Obsoletes: python{X_Y}-foo(x86_64) < 6-1.fc66' not in lines assert len(lines) == 3 @pytest.mark.skip("py_provides behaves differently for alternative Python stacks") @pytest.mark.parametrize('rhel', [None, 13]) def test_py_provides_python3_epoched(rhel): lines = rpm_eval('%py_provides python3-foo', epoch='1', version='6', release='1.fc66', rhel=rhel) assert 'Provides: python3-foo = 1:6-1.fc66' in lines assert 'Provides: python-foo = 1:6-1.fc66' in lines assert f'Provides: python{X_Y}-foo = 1:6-1.fc66' in lines if rhel: assert f'Obsoletes: python{X_Y}-foo < 1:6-1.fc66' in lines assert len(lines) == 4 else: assert len(lines) == 3 @pytest.mark.skip("py_provides behaves differently for alternative Python stacks") @pytest.mark.parametrize('rhel', [None, 13]) def test_py_provides_python3X(rhel): lines = rpm_eval(f'%py_provides python{X_Y}-foo', version='6', release='1.fc66', rhel=rhel) assert f'Provides: python{X_Y}-foo = 6-1.fc66' in lines assert 'Provides: python-foo = 6-1.fc66' in lines assert 'Provides: python3-foo = 6-1.fc66' in lines assert len(lines) == 3 @pytest.mark.skip("py_provides behaves differently for alternative Python stacks") @pytest.mark.parametrize('rhel', [None, 27]) def test_py_provides_python3X_epoched(rhel): lines = rpm_eval(f'%py_provides python{X_Y}-foo', epoch='1', version='6', release='1.fc66', rhel=rhel) assert f'Provides: python{X_Y}-foo = 1:6-1.fc66' in lines assert 'Provides: python-foo = 1:6-1.fc66' in lines assert 'Provides: python3-foo = 1:6-1.fc66' in lines assert len(lines) == 3 @pytest.mark.skip("py_provides behaves differently for alternative Python stacks") @pytest.mark.parametrize('rhel', [None, 2]) def test_py_provides_doubleuse(rhel): lines = rpm_eval('%{py_provides python3-foo}%{py_provides python3-foo}', version='6', release='1.fc66', rhel=rhel) assert 'Provides: python3-foo = 6-1.fc66' in lines assert 'Provides: python-foo = 6-1.fc66' in lines assert f'Provides: python{X_Y}-foo = 6-1.fc66' in lines if rhel: assert f'Obsoletes: python{X_Y}-foo < 6-1.fc66' in lines assert len(lines) == 8 assert len(set(lines)) == 4 else: assert len(lines) == 6 assert len(set(lines)) == 3 @pytest.mark.skip("py_provides behaves differently for alternative Python stacks") @pytest.mark.parametrize('rhel', [None, 2]) def test_py_provides_with_evr(rhel): lines = rpm_eval('%py_provides python3-foo 123', version='6', release='1.fc66', rhel=rhel) assert 'Provides: python3-foo = 123' in lines assert 'Provides: python-foo = 123' in lines assert f'Provides: python{X_Y}-foo = 123' in lines if rhel: assert f'Obsoletes: python{X_Y}-foo < 123' in lines assert len(lines) == 4 else: assert len(lines) == 3 def test_python_wheel_pkg_prefix(): assert rpm_eval('%python_wheel_pkg_prefix', fedora='44', rhel=None, eln=None) == ['python'] assert rpm_eval('%python_wheel_pkg_prefix', fedora='44', rhel=None, eln=None, python3_pkgversion='3.9') == ['python'] assert rpm_eval('%python_wheel_pkg_prefix', fedora=None, rhel='1', eln='1') == ['python'] assert rpm_eval('%python_wheel_pkg_prefix', fedora=None, rhel='1', eln=None) == ['python3.12'] assert rpm_eval('%python_wheel_pkg_prefix', fedora=None, rhel='1', eln=None, python3_pkgversion='3.10') == ['python3.10'] assert rpm_eval('%python_wheel_pkg_prefix', fedora=None, rhel='1', eln=None, python3_pkgversion='3.12') == ['python3.12'] def test_python_wheel_dir(): assert rpm_eval('%python_wheel_dir', fedora='44', rhel=None, eln=None) == ['/usr/share/python-wheels'] assert rpm_eval('%python_wheel_dir', fedora='44', rhel=None, eln=None, python3_pkgversion='3.9') == ['/usr/share/python-wheels'] assert rpm_eval('%python_wheel_dir', fedora=None, rhel='1', eln='1') == ['/usr/share/python-wheels'] assert rpm_eval('%python_wheel_dir', fedora=None, rhel='1', eln=None) == ['/usr/share/python3.12-wheels'] assert rpm_eval('%python_wheel_dir', fedora=None, rhel='1', eln=None, python3_pkgversion='3.10') == ['/usr/share/python3.10-wheels'] assert rpm_eval('%python_wheel_dir', fedora=None, rhel='1', eln=None, python3_pkgversion='3.12') == ['/usr/share/python3.12-wheels'] def test_pytest_passes_options_naturally(): lines = rpm_eval('%pytest -k foo') assert '/usr/bin/pytest-3.12 -k foo' in lines[-1] def test_pytest_different_command(): lines = rpm_eval('%pytest', __pytest='pytest-3') assert 'pytest-3' in lines[-1] def test_pytest_command_suffix(): lines = rpm_eval('%pytest -v') assert '/usr/bin/pytest-3.12 -v' in lines[-1] # this test does not require alternate Pythons to be installed @pytest.mark.parametrize('version', ['3.6', '3.7', '3.12']) def test_pytest_command_suffix_alternate_pkgversion(version): lines = rpm_eval('%pytest -v', python3_pkgversion=version, python3_version=version) assert f'/usr/bin/pytest-{version} -v' in lines[-1] @pytest.mark.skip("This functionality is not present in RHEL 8") def test_pytest_sets_pytest_xdist_auto_num_workers(): lines = rpm_eval('%pytest', _smp_build_ncpus=2) assert 'PYTEST_XDIST_AUTO_NUM_WORKERS=2' in '\n'.join(lines) def test_pytest_undefined_addopts_are_not_set(): lines = rpm_eval('%pytest', __pytest_addopts=None) assert 'PYTEST_ADDOPTS' not in '\n'.join(lines) def test_pytest_defined_addopts_are_set(): lines = rpm_eval('%pytest', __pytest_addopts="--ignore=stuff") assert 'PYTEST_ADDOPTS="${PYTEST_ADDOPTS:-} --ignore=stuff"' in '\n'.join(lines) @pytest.mark.parametrize('__pytest_addopts', ['--macronized-option', 'x y z', None]) def test_pytest_addopts_preserves_envvar(__pytest_addopts): # this is the line a packager might put in the spec file before running %pytest: spec_line = 'export PYTEST_ADDOPTS="--exported-option1 --exported-option2"' # instead of actually running /usr/bin/pytest, # we run a small shell script that echoes the tested value for inspection lines = rpm_eval('%pytest', __pytest_addopts=__pytest_addopts, __pytest="sh -c 'echo $PYTEST_ADDOPTS'") echoed = shell_stdout('\n'.join([spec_line] + lines)) # assert all values were echoed assert '--exported-option1' in echoed assert '--exported-option2' in echoed if __pytest_addopts is not None: assert __pytest_addopts in echoed # assert the options are separated assert 'option--' not in echoed assert 'z--' not in echoed @pytest.mark.skip("RHEL 8 predates py3_test_envvars") @pytest.mark.parametrize('__pytest_addopts', ['-X', None]) def test_py3_test_envvars(lib, __pytest_addopts): lines = rpm_eval('%{py3_test_envvars}\\\n%{python3} -m unittest', buildroot='BUILDROOT', _smp_build_ncpus='3', __pytest_addopts=__pytest_addopts) assert all(l.endswith('\\') for l in lines[:-1]) stripped_lines = [l.strip(' \\') for l in lines] sitearch = f'BUILDROOT/usr/{lib}/python{X_Y}/site-packages' sitelib = f'BUILDROOT/usr/lib/python{X_Y}/site-packages' assert f'PYTHONPATH="${{PYTHONPATH:-{sitearch}:{sitelib}}}"' in stripped_lines assert 'PATH="BUILDROOT/usr/bin:$PATH"' in stripped_lines assert 'CFLAGS="${CFLAGS:-${RPM_OPT_FLAGS}}" LDFLAGS="${LDFLAGS:-${RPM_LD_FLAGS}}"' in stripped_lines assert 'PYTHONDONTWRITEBYTECODE=1' in stripped_lines assert 'PYTEST_XDIST_AUTO_NUM_WORKERS=3' in stripped_lines if __pytest_addopts: assert f'PYTEST_ADDOPTS="${{PYTEST_ADDOPTS:-}} {__pytest_addopts}"' in stripped_lines else: assert 'PYTEST_ADDOPTS' not in ''.join(lines) assert stripped_lines[-1] == '/usr/bin/python3.12 -m unittest' def test_pypi_source_default_name(): urls = rpm_eval('%pypi_source', name='foo', version='6') assert urls == ['https://files.pythonhosted.org/packages/source/f/foo/foo-6.tar.gz'] def test_pypi_source_default_srcname(): urls = rpm_eval('%pypi_source', name='python-foo', srcname='foo', version='6') assert urls == ['https://files.pythonhosted.org/packages/source/f/foo/foo-6.tar.gz'] def test_pypi_source_default_pypi_name(): urls = rpm_eval('%pypi_source', name='python-foo', pypi_name='foo', version='6') assert urls == ['https://files.pythonhosted.org/packages/source/f/foo/foo-6.tar.gz'] def test_pypi_source_default_name_uppercase(): urls = rpm_eval('%pypi_source', name='Foo', version='6') assert urls == ['https://files.pythonhosted.org/packages/source/F/Foo/Foo-6.tar.gz'] def test_pypi_source_provided_name(): urls = rpm_eval('%pypi_source foo', name='python-bar', pypi_name='bar', version='6') assert urls == ['https://files.pythonhosted.org/packages/source/f/foo/foo-6.tar.gz'] def test_pypi_source_provided_name_version(): urls = rpm_eval('%pypi_source foo 6', name='python-bar', pypi_name='bar', version='3') assert urls == ['https://files.pythonhosted.org/packages/source/f/foo/foo-6.tar.gz'] def test_pypi_source_provided_name_version_ext(): url = rpm_eval('%pypi_source foo 6 zip', name='python-bar', pypi_name='bar', version='3') assert url == ['https://files.pythonhosted.org/packages/source/f/foo/foo-6.zip'] def test_pypi_source_prerelease(): urls = rpm_eval('%pypi_source', name='python-foo', pypi_name='foo', version='6~b2') assert urls == ['https://files.pythonhosted.org/packages/source/f/foo/foo-6b2.tar.gz'] def test_pypi_source_explicit_tilde(): urls = rpm_eval('%pypi_source foo 6~6', name='python-foo', pypi_name='foo', version='6') assert urls == ['https://files.pythonhosted.org/packages/source/f/foo/foo-6~6.tar.gz'] @pytest.mark.skip("%py3_shebang_fix has a complicated evaluation in RHEL 8 due to differences between Python stacks") def test_py3_shebang_fix(): cmd = rpm_eval('%py3_shebang_fix arg1 arg2 arg3')[-1].strip() assert cmd == '/usr/bin/python3.12 -B /usr/lib/rpm/redhat/pathfix.py -pni /usr/bin/python3.12 $shebang_flags arg1 arg2 arg3' def test_py3_shebang_fix_default_shebang_flags(): lines = rpm_eval('%py3_shebang_fix arg1 arg2') lines[-1] = 'echo $shebang_flags' assert shell_stdout('\n'.join(lines)) == f'-kas{safe_path_flag(X_Y)}' def test_py3_shebang_fix_custom_shebang_flags(): lines = rpm_eval('%py3_shebang_fix arg1 arg2', py3_shebang_flags='Es') lines[-1] = 'echo $shebang_flags' assert shell_stdout('\n'.join(lines)) == '-kaEs' @pytest.mark.parametrize('_py3_shebang_s', [None, '%{nil}']) def test_py3_shebang_fix_undefined_py3_shebang_s(_py3_shebang_s): lines = rpm_eval('%py3_shebang_fix arg1 arg2', _py3_shebang_s=_py3_shebang_s) lines[-1] = 'echo $shebang_flags' expected = f'-ka{safe_path_flag(X_Y)}' if safe_path_flag(X_Y) else '-k' assert shell_stdout('\n'.join(lines)) == expected @pytest.mark.parametrize('_py3_shebang_P', [None, '%{nil}']) def test_py3_shebang_fix_undefined_py3_shebang_P(_py3_shebang_P): lines = rpm_eval('%py3_shebang_fix arg1 arg2', _py3_shebang_P=_py3_shebang_P) lines[-1] = 'echo $shebang_flags' assert shell_stdout('\n'.join(lines)) == '-kas' @pytest.mark.parametrize('_py3_shebang_s', [None, '%{nil}']) @pytest.mark.parametrize('_py3_shebang_P', [None, '%{nil}']) def test_py3_shebang_fix_undefined_py3_shebang_sP(_py3_shebang_s, _py3_shebang_P): lines = rpm_eval('%py3_shebang_fix arg1 arg2', _py3_shebang_s=_py3_shebang_s, _py3_shebang_P=_py3_shebang_P) lines[-1] = 'echo $shebang_flags' assert shell_stdout('\n'.join(lines)) == '-k' @pytest.mark.parametrize('flags', [None, '%{nil}']) def test_py3_shebang_fix_no_shebang_flags(flags): lines = rpm_eval('%py3_shebang_fix arg1 arg2', py3_shebang_flags=flags) lines[-1] = 'echo $shebang_flags' assert shell_stdout('\n'.join(lines)) == '-k' @pytest.mark.skip("%py_shebang_fix has a complicated evaluation in RHEL 8 due to differences between Python stacks") def test_py_shebang_fix_custom_python(): cmd = rpm_eval('%py_shebang_fix arg1 arg2 arg3', __python='/usr/bin/pypy')[-1].strip() assert cmd == '/usr/bin/pypy -B /usr/lib/rpm/redhat/pathfix.py -pni /usr/bin/pypy $shebang_flags arg1 arg2 arg3' def test_pycached_in_sitelib(): lines = rpm_eval('%pycached %{python3_sitelib}/foo*.py') assert lines == [ f'/usr/lib/python{X_Y}/site-packages/foo*.py', f'/usr/lib/python{X_Y}/site-packages/__pycache__/foo*.cpython-{XY}{{,.opt-?}}.pyc' ] def test_pycached_in_sitearch(lib): lines = rpm_eval('%pycached %{python3_sitearch}/foo*.py') assert lines == [ f'/usr/{lib}/python{X_Y}/site-packages/foo*.py', f'/usr/{lib}/python{X_Y}/site-packages/__pycache__/foo*.cpython-{XY}{{,.opt-?}}.pyc' ] # this test does not require alternate Pythons to be installed @pytest.mark.parametrize('version', ['3.6', '3.7', '3.12']) def test_pycached_with_alternate_version(version): version_nodot = version.replace('.', '') lines = rpm_eval(f'%pycached /usr/lib/python{version}/site-packages/foo*.py') assert lines == [ f'/usr/lib/python{version}/site-packages/foo*.py', f'/usr/lib/python{version}/site-packages/__pycache__/foo*.cpython-{version_nodot}{{,.opt-?}}.pyc' ] def test_pycached_in_custom_dir(): lines = rpm_eval('%pycached /bar/foo*.py') assert lines == [ '/bar/foo*.py', '/bar/__pycache__/foo*.cpython-3*{,.opt-?}.pyc' ] def test_pycached_with_exclude(): lines = rpm_eval('%pycached %exclude %{python3_sitelib}/foo*.py') assert lines == [ f'%exclude /usr/lib/python{X_Y}/site-packages/foo*.py', f'%exclude /usr/lib/python{X_Y}/site-packages/__pycache__/foo*.cpython-{XY}{{,.opt-?}}.pyc' ] def test_pycached_fails_with_extension_glob(): lines = rpm_eval('%pycached %{python3_sitelib}/foo.py*', fails=True) assert lines[0] == 'error: %pycached can only be used with paths explicitly ending with .py' @pytest.mark.skip("Python extras subpackages are not handled in RHEL 8") def test_python_extras_subpkg_i(): lines = rpm_eval('%python_extras_subpkg -n python3-setuptools_scm -i %{python3_sitelib}/*.egg-info toml yaml', version='6', release='7') expected = textwrap.dedent(f""" %package -n python3-setuptools_scm+toml Summary: Metapackage for python3-setuptools_scm: toml extras Requires: python3-setuptools_scm = 6-7 %description -n python3-setuptools_scm+toml This is a metapackage bringing in toml extras requires for python3-setuptools_scm. It makes sure the dependencies are installed. %files -n python3-setuptools_scm+toml %ghost /usr/lib/python{X_Y}/site-packages/*.egg-info %package -n python3-setuptools_scm+yaml Summary: Metapackage for python3-setuptools_scm: yaml extras Requires: python3-setuptools_scm = 6-7 %description -n python3-setuptools_scm+yaml This is a metapackage bringing in yaml extras requires for python3-setuptools_scm. It makes sure the dependencies are installed. %files -n python3-setuptools_scm+yaml %ghost /usr/lib/python{X_Y}/site-packages/*.egg-info """).lstrip().splitlines() assert lines == expected @pytest.mark.skip("Python extras subpackages are not handled in RHEL 8") def test_python_extras_subpkg_f(): lines = rpm_eval('%python_extras_subpkg -n python3-setuptools_scm -f ghost_filelist toml yaml', version='6', release='7') expected = textwrap.dedent(f""" %package -n python3-setuptools_scm+toml Summary: Metapackage for python3-setuptools_scm: toml extras Requires: python3-setuptools_scm = 6-7 %description -n python3-setuptools_scm+toml This is a metapackage bringing in toml extras requires for python3-setuptools_scm. It makes sure the dependencies are installed. %files -n python3-setuptools_scm+toml -f ghost_filelist %package -n python3-setuptools_scm+yaml Summary: Metapackage for python3-setuptools_scm: yaml extras Requires: python3-setuptools_scm = 6-7 %description -n python3-setuptools_scm+yaml This is a metapackage bringing in yaml extras requires for python3-setuptools_scm. It makes sure the dependencies are installed. %files -n python3-setuptools_scm+yaml -f ghost_filelist """).lstrip().splitlines() assert lines == expected @pytest.mark.skip("Python extras subpackages are not handled in RHEL 8") def test_python_extras_subpkg_F(): lines = rpm_eval('%python_extras_subpkg -n python3-setuptools_scm -F toml yaml', version='6', release='7') expected = textwrap.dedent(f""" %package -n python3-setuptools_scm+toml Summary: Metapackage for python3-setuptools_scm: toml extras Requires: python3-setuptools_scm = 6-7 %description -n python3-setuptools_scm+toml This is a metapackage bringing in toml extras requires for python3-setuptools_scm. It makes sure the dependencies are installed. %package -n python3-setuptools_scm+yaml Summary: Metapackage for python3-setuptools_scm: yaml extras Requires: python3-setuptools_scm = 6-7 %description -n python3-setuptools_scm+yaml This is a metapackage bringing in yaml extras requires for python3-setuptools_scm. It makes sure the dependencies are installed. """).lstrip().splitlines() assert lines == expected @pytest.mark.skip("Python extras subpackages are not handled in RHEL 8") def test_python_extras_subpkg_underscores(): lines = rpm_eval('%python_extras_subpkg -n python3-webscrapbook -F adhoc_ssl', version='0.33.3', release='1.fc33') expected = textwrap.dedent(f""" %package -n python3-webscrapbook+adhoc_ssl Summary: Metapackage for python3-webscrapbook: adhoc_ssl extras Requires: python3-webscrapbook = 0.33.3-1.fc33 %description -n python3-webscrapbook+adhoc_ssl This is a metapackage bringing in adhoc_ssl extras requires for python3-webscrapbook. It makes sure the dependencies are installed. """).lstrip().splitlines() assert lines == expected @pytest.mark.skip("Python extras subpackages are not handled in RHEL 8") @pytest.mark.parametrize('sep', [pytest.param(('', ' ', ' ', ''), id='spaces'), pytest.param(('', ',', ',', ''), id='commas'), pytest.param(('', ',', ',', ','), id='commas-trailing'), pytest.param((',', ',', ',', ''), id='commas-leading'), pytest.param((',', ',', ',', ','), id='commas-trailing-leading'), pytest.param(('', ',', ' ', ''), id='mixture'), pytest.param((' ', ' ', '\t\t, ', '\t'), id='chaotic-good'), pytest.param(('', '\t ,, \t\r ', ',,\t , ', ',,'), id='chaotic-evil')]) def test_python_extras_subpkg_arg_separators(sep): lines = rpm_eval('%python_extras_subpkg -n python3-hypothesis -F {}cli{}ghostwriter{}pytz{}'.format(*sep), version='6.6.0', release='1.fc35') expected = textwrap.dedent(f""" %package -n python3-hypothesis+cli Summary: Metapackage for python3-hypothesis: cli extras Requires: python3-hypothesis = 6.6.0-1.fc35 %description -n python3-hypothesis+cli This is a metapackage bringing in cli extras requires for python3-hypothesis. It makes sure the dependencies are installed. %package -n python3-hypothesis+ghostwriter Summary: Metapackage for python3-hypothesis: ghostwriter extras Requires: python3-hypothesis = 6.6.0-1.fc35 %description -n python3-hypothesis+ghostwriter This is a metapackage bringing in ghostwriter extras requires for python3-hypothesis. It makes sure the dependencies are installed. %package -n python3-hypothesis+pytz Summary: Metapackage for python3-hypothesis: pytz extras Requires: python3-hypothesis = 6.6.0-1.fc35 %description -n python3-hypothesis+pytz This is a metapackage bringing in pytz extras requires for python3-hypothesis. It makes sure the dependencies are installed. """).lstrip().splitlines() assert lines == expected @pytest.mark.skip("Python extras subpackages are not handled in RHEL 8") @pytest.mark.parametrize('basename_len', [1, 10, 30, 45, 78]) @pytest.mark.parametrize('extra_len', [1, 13, 28, 52, 78]) def test_python_extras_subpkg_description_wrapping(basename_len, extra_len): basename = 'x' * basename_len extra = 'y' * extra_len lines = rpm_eval(f'%python_extras_subpkg -n {basename} -F {extra}', version='6', release='7') for idx, line in enumerate(lines): if line.startswith('%description'): start = idx + 1 lines = lines[start:] assert all(len(l) < 80 for l in lines) assert len(lines) < 6 if len(" ".join(lines[:-1])) < 80: assert len(lines) == 2 expected_singleline = (f"This is a metapackage bringing in {extra} extras " f"requires for {basename}. " f"It makes sure the dependencies are installed.") description_singleline = " ".join(lines) assert description_singleline == expected_singleline unversioned_macros = pytest.mark.parametrize('macro', [ '%__python', '%python', '%python_version', '%python_version_nodots', '%python_sitelib', '%python_sitearch', '%python_platform', '%python_platform_triplet', '%python_ext_suffix', '%python_cache_tag', '%py_shebang_fix', '%py_build', '%py_build_egg', '%py_build_wheel', '%py_install', '%py_install_egg', '%py_install_wheel', '%py_check_import', '%py_test_envvars', ]) @pytest.mark.skip("Not applicable for python3.12-rpm-macros") @unversioned_macros def test_unversioned_python_errors(macro): lines = rpm_eval(macro, fails=True) assert lines[0] == ( 'error: attempt to use unversioned python, ' 'define %__python to /usr/bin/python2 or /usr/bin/python3 explicitly' ) # when the macros are %global, the error is longer # we deliberately allow this extra line to be optional if len(lines) > 1: # the failed macro is not unnecessarily our tested macro pattern = r'error: Macro %\S+ failed to expand' assert re.match(pattern, lines[1]) # but there should be no more lines assert len(lines) < 3 @pytest.mark.skip("Not applicable for python3.12-rpm-macros") @unversioned_macros def test_unversioned_python_works_when_defined(macro): macro3 = macro.replace('python', 'python3').replace('py_', 'py3_') assert rpm_eval(macro, __python='/usr/bin/python3.12') == rpm_eval(macro3) # we could rework the test for multiple architectures, but the Fedora CI currently only runs on x86_64 x86_64_only = pytest.mark.skipif(platform.machine() != "x86_64", reason="works on x86_64 only") @x86_64_only def test_platform_triplet(): assert rpm_eval("%python3_platform_triplet") == ["x86_64-linux-gnu"] @x86_64_only def test_ext_suffix(): assert rpm_eval("%python3_ext_suffix") == [f".cpython-{XY}-x86_64-linux-gnu.so"] def test_cache_tag(): assert rpm_eval("%python3_cache_tag") == [f"cpython-{XY}"] def test_cache_tag_alternate_python(alt_x_y, alt_xy): assert rpm_eval("%python_cache_tag", __python=f"/usr/bin/python{alt_x_y}") == [f"cpython-{alt_xy}"] def test_cache_tag_alternate_python3(alt_x_y, alt_xy): assert rpm_eval("%python3_cache_tag", __python3=f"/usr/bin/python{alt_x_y}") == [f"cpython-{alt_xy}"] @pytest.mark.skip("Throws ModuleNotFoundError(distutils) when run with Python 3.12, expected") def test_python_sitelib_value_python3(): macro = '%python_sitelib' assert rpm_eval(macro, __python='%__python3') == [f'/usr/lib/python{X_Y}/site-packages'] def test_python_sitelib_value_alternate_python(alt_x_y): macro = '%python_sitelib' assert rpm_eval(macro, __python=f'/usr/bin/python{alt_x_y}') == [f'/usr/lib/python{alt_x_y}/site-packages'] def test_python3_sitelib_value_default(): macro = '%python3_sitelib' assert rpm_eval(macro) == [f'/usr/lib/python{X_Y}/site-packages'] def test_python3_sitelib_value_alternate_python(alt_x_y): macro = '%python3_sitelib' assert (rpm_eval(macro, __python3=f'/usr/bin/python{alt_x_y}') == rpm_eval(macro, python3_pkgversion=alt_x_y) == [f'/usr/lib/python{alt_x_y}/site-packages']) def test_python3_sitelib_value_alternate_prefix(): macro = '%python3_sitelib' assert rpm_eval(macro, _prefix='/app') == [f'/app/lib/python{X_Y}/site-packages'] @pytest.mark.skip("Throws ModuleNotFoundError(distutils) when run with Python 3.12, expected") def test_python_sitearch_value_python3(lib): macro = '%python_sitearch' assert rpm_eval(macro, __python='%__python3') == [f'/usr/{lib}/python{X_Y}/site-packages'] def test_python_sitearch_value_alternate_python(lib, alt_x_y): macro = '%python_sitearch' assert rpm_eval(macro, __python=f'/usr/bin/python{alt_x_y}') == [f'/usr/{lib}/python{alt_x_y}/site-packages'] def test_python3_sitearch_value_default(lib): macro = '%python3_sitearch' assert rpm_eval(macro) == [f'/usr/{lib}/python{X_Y}/site-packages'] def test_python3_sitearch_value_alternate_python(lib, alt_x_y): macro = '%python3_sitearch' assert (rpm_eval(macro, __python3=f'/usr/bin/python{alt_x_y}') == rpm_eval(macro, python3_pkgversion=alt_x_y) == [f'/usr/{lib}/python{alt_x_y}/site-packages']) def test_python3_sitearch_value_alternate_prefix(lib): macro = '%python3_sitearch' assert rpm_eval(macro, _prefix='/app') == [f'/app/{lib}/python{X_Y}/site-packages'] @pytest.mark.parametrize( 'args, expected_args', [ ('six', 'six'), ('-f foo.txt', '-f foo.txt'), ('-t -f foo.txt six, seven', '-t -f foo.txt six, seven'), ('-e "foo*" -f foo.txt six, seven', '-e "foo*" -f foo.txt six, seven'), ('six.quarter six.half,, SIX', 'six.quarter six.half,, SIX'), ('-f foo.txt six\nsix.half\nSIX', '-f foo.txt six six.half SIX'), ('six \\ -e six.half', 'six -e six.half'), ] ) @pytest.mark.parametrize('__python3', [None, f'/usr/bin/python{X_Y}', '/usr/bin/pythonX.Y']) def test_py3_check_import(args, expected_args, __python3, lib): x_y = X_Y macros = { 'buildroot': 'BUILDROOT', '_rpmconfigdir': 'RPMCONFIGDIR', } if __python3 is not None: if 'X.Y' in __python3: __python3 = __python3.replace('X.Y', get_alt_x_y()) macros['__python3'] = __python3 # If the __python3 command has version at the end, parse it and expect it. # Note that the command is used to determine %python3_sitelib and %python3_sitearch, # so we only test known CPython schemes here and not PyPy for simplicity. if (match := re.match(r'.+python(\d+\.\d+)$', __python3)): x_y = match.group(1) invocation = '%{py3_check_import ' + args +'}' lines = rpm_eval(invocation, **macros) # An equality check is a bit inflexible here, # every time we change the macro we need to change this test. # However actually executing it and verifying the result is much harder :/ # At least, let's make the lines saner to check: lines = [line.rstrip('\\').strip() for line in lines] expected = textwrap.dedent(fr""" PATH="BUILDROOT/usr/bin:$PATH" PYTHONPATH="${{PYTHONPATH:-BUILDROOT/usr/{lib}/python{x_y}/site-packages:BUILDROOT/usr/lib/python{x_y}/site-packages}}" _PYTHONSITE="BUILDROOT/usr/{lib}/python{x_y}/site-packages:BUILDROOT/usr/lib/python{x_y}/site-packages" PYTHONDONTWRITEBYTECODE=1 {__python3 or '/usr/bin/python3.12'} -s{safe_path_flag(x_y)} RPMCONFIGDIR/redhat/import_all_modules_py3_12.py {expected_args} """) assert lines == expected.splitlines() @pytest.mark.parametrize( 'shebang_flags_value, expected_shebang_flags', [ ('sP', '-sP'), ('s', '-s'), ('%{nil}', ''), (None, ''), ('Es', '-Es'), ] ) def test_py3_check_import_respects_shebang_flags(shebang_flags_value, expected_shebang_flags, lib): macros = { '_rpmconfigdir': 'RPMCONFIGDIR', '__python3': '/usr/bin/python3', 'py3_shebang_flags': shebang_flags_value, } lines = rpm_eval('%py3_check_import sys', **macros) # Compare the last line of the command, that's where lua part is evaluated expected = f'/usr/bin/python3 {expected_shebang_flags} RPMCONFIGDIR/redhat/import_all_modules_py3_12.py sys' assert lines[-1].strip() == expected