diff --git a/.fmf/version b/.fmf/version new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/.fmf/version @@ -0,0 +1 @@ +1 diff --git a/gating.yaml b/gating.yaml index eb7c84f..d74bf08 100644 --- a/gating.yaml +++ b/gating.yaml @@ -1,6 +1,8 @@ --- !Policy + product_versions: - rhel-8 decision_context: osci_compose_gate rules: - - !PassingTestCaseRule {test_case_name: baseos-ci.brew-build.tier1.functional} + - !PassingTestCaseRule {test_case_name: osci.brew-build.tier0.functional} + diff --git a/plan.fmf b/plan.fmf new file mode 100644 index 0000000..c76fd4e --- /dev/null +++ b/plan.fmf @@ -0,0 +1,24 @@ +execute: + how: tmt + +discover: + how: shell + tests: + - name: pytest + test: PYTHONPATH=/usr/lib/rpm/redhat ALTERNATE_PYTHON_VERSION=SKIP pytest-3 -v + path: /tests + - name: manual_byte_compilation + test: rpmbuild -ba pythontest.spec + path: /tests + +prepare: + - how: install + package: + - rpm-build + - python-rpm-macros + - python3-rpm-macros + - platform-python-devel + - python3-pytest + - dnf + - how: shell + script: dnf upgrade -y diff --git a/tests/pythontest.spec b/tests/pythontest.spec new file mode 100644 index 0000000..459b7ca --- /dev/null +++ b/tests/pythontest.spec @@ -0,0 +1,33 @@ +%global basedir /opt/test/byte_compilation + +Name: pythontest +Version: 0 +Release: 0 +Summary: ... +License: MIT +BuildRequires: platform-python-devel + +%description +... + +%install +mkdir -p %{buildroot}%{basedir}/directory/to/test/recursion + +echo "print()" > %{buildroot}%{basedir}/file.py +echo "print()" > %{buildroot}%{basedir}/directory/to/test/recursion/file_in_dir.py + +%py_byte_compile %{python3} %{buildroot}%{basedir}/file.py +%py_byte_compile %{python3} %{buildroot}%{basedir}/directory + +%check +# Count .py and .pyc files +PY=$(find %{buildroot}%{basedir} -name "*.py" | wc -l) +PYC=$(find %{buildroot}%{basedir} -name "*.pyc" | wc -l) + +# Every .py file should be byte-compiled to two .pyc files (optimization level 0 and 1) +# so we should have two times more .pyc files than .py files +test $(expr $PY \* 2) -eq $PYC + +%files +%pycached %{basedir}/file.py +%pycached %{basedir}/directory/to/test/recursion/file_in_dir.py diff --git a/tests/test_evals.py b/tests/test_evals.py new file mode 100644 index 0000000..8468574 --- /dev/null +++ b/tests/test_evals.py @@ -0,0 +1,462 @@ +import os +import subprocess +import platform +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. +# To get 'em all, run: +# ls -1 macros.* | tr "\n" ":" +# and then: +# TESTED_FILES="" pytest -v +# or both combined: +# TESTED_FILES=$(ls -1 macros.* | tr "\n" ":") pytest -v +# Remember that some tests might need more macros files than just +# the local ones. +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] + # python3.6 doesn't have "text=True" which is used in Fedora + cp = subprocess.run(cmd, env={**os.environ, 'LANG': 'C.utf-8'}, encoding='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() + + +def shell_stdout(script): + # python3.6 doesn't have "text=True" which is used in Fedora + return subprocess.check_output(script, + env={**os.environ, 'LANG': 'C.utf-8'}, + encoding='utf-8', + shell=True).rstrip() + + +def older_rhel87(): + try: + _, r, _ = platform.dist() + return r != '8.10' and float(r) < 8.7 + except Exception: + return False + +@pytest.mark.parametrize('argument, result', [ + ('a', 'a'), + ('a-a', 'a-a'), + ('a_a', 'a-a'), + pytest.param('a.a', 'a-a', marks=pytest.mark.skip(reason="stay old-RHEL compatible")), + pytest.param('a.a', 'a.a'), + ('a---a', 'a-a'), + ('a-_-a', 'a-a'), + ('a-_-a', 'a-a'), + pytest.param('a[b]', 'a[b]', marks=pytest.mark.skip(reason="not planned for RHEL")), + pytest.param('Aha[Boom]', 'aha[boom]', marks=pytest.mark.skip(reason="not planned for RHEL")), + pytest.param('a.a[b.b]', 'a-a[b-b]', marks=pytest.mark.skip(reason="not planned for RHEL")), +]) +def test_pydist_name(argument, result): + assert rpm_eval(f'%py_dist_name {argument}') == [result] + + +@pytest.mark.skip(reason="not planned for RHEL") +def test_py2_dist(): + assert rpm_eval(f'%py2_dist Aha[Boom] a') == ['python2dist(aha[boom]) python2dist(a)'] + +@pytest.mark.skip(reason="not planned for RHEL") +def test_py3_dist(): + assert rpm_eval(f'%py3_dist Aha[Boom] a') == ['python3dist(aha[boom]) python3dist(a)'] + +@pytest.mark.skip(reason="not planned for RHEL") +def test_py3_dist_with_python3_pkgversion_redefined(): + assert rpm_eval(f'%py3_dist Aha[Boom] a', + python3_pkgversion="3.6") == ['python3.6dist(aha[boom]) python3.6dist(a)'] + + +def test_python_provide_python(): + assert rpm_eval('%python_provide python-foo') == ['Provides: python2-foo = %{version}-%{release}'] + + +def test_python_provide_python3(): + assert rpm_eval('%python_provide python3-foo', version='6', release='1.fc66') == [] + + +def test_python_provide_python3_epoched(): + assert rpm_eval('%python_provide python3-foo', epoch='1', version='6', release='1.fc66') == [] + + +def test_python_provide_python3X(): + assert rpm_eval(f'%python_provide python{X_Y}-foo', version='6', + release='1.fc66') == ['%python_provide: ERROR: python3.6-foo not recognized.'] + + +def test_python_provide_python3X_epoched(): + assert rpm_eval(f'%python_provide python{X_Y}-foo', epoch='1', version='6', + release='1.fc66') == ['%python_provide: ERROR: python3.6-foo not recognized.'] + + +def test_python_provide_doubleuse(): + lines = rpm_eval('%{python_provide python3-foo}%{python_provide python3-foo}', + version='6', release='1.fc66') == [] + + +def test_py_provides_python(): + lines = rpm_eval('%py_provides python-foo', version='6', release='1.fc66') + assert 'Provides: python-foo = 6-1.fc66' in lines + assert len(lines) == 1 + + +def test_py_provides_whatever(): + lines = rpm_eval('%py_provides whatever', version='6', release='1.fc66') + assert 'Provides: whatever = 6-1.fc66' in lines + assert len(lines) == 1 + + +def test_py_provides_python3(): + lines = rpm_eval('%py_provides python3-foo', version='6', release='1.fc66') + assert 'Provides: python3-foo = 6-1.fc66' in lines + # difference RHEL vs Fedora: python-foo format is not allowed + assert 'Provides: python-foo = 6-1.fc66' not in lines + assert len(lines) == 1 + + +def test_py_provides_python3_epoched(): + lines = rpm_eval('%py_provides python3-foo', epoch='1', version='6', release='1.fc66') + assert 'Provides: python3-foo = 1:6-1.fc66' in lines + # difference RHEL vs Fedora: python-foo format is not allowed + assert 'Provides: python-foo = 1:6-1.fc66' not in lines + assert len(lines) == 1 + + +def test_py_provides_python3X(): + lines = rpm_eval(f'%py_provides python{X_Y}-foo', version='6', release='1.fc66') + assert f'Provides: python{X_Y}-foo = 6-1.fc66' in lines + # difference RHEL vs Fedora: python-foo format is not allowed + assert 'Provides: python-foo = 6-1.fc66' not in lines + assert len(lines) == 1 + + +def test_py_provides_python3X_epoched(): + lines = rpm_eval(f'%py_provides python{X_Y}-foo', epoch='1', version='6', release='1.fc66') + assert f'Provides: python{X_Y}-foo = 1:6-1.fc66' in lines + # difference RHEL vs Fedora: python-foo format is not allowed + assert 'Provides: python-foo = 1:6-1.fc66' not in lines + assert len(lines) == 1 + + +def test_py_provides_doubleuse(): + lines = rpm_eval('%{py_provides python3-foo}%{py_provides python3-foo}', + version='6', release='1.fc66') + assert 'Provides: python3-foo = 6-1.fc66' in lines + # difference RHEL vs Fedora: python-foo format is not allowed + assert 'Provides: python-foo = 6-1.fc66' not in lines + assert len(lines) == 2 + assert len(set(lines)) == 1 + + +def test_py_provides_with_evr(): + lines = rpm_eval('%py_provides python3-foo 123', + version='6', release='1.fc66') + assert 'Provides: python3-foo = 123' in lines + assert len(lines) == 1 + + +def test_pytest_passes_options_naturally(): + lines = rpm_eval('%pytest -k foo') + # difference RHEL vs Fedora: pytest-3 binary name + assert '/usr/bin/pytest-3 -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') + # difference RHEL vs Fedora: pytest-3 binary name + assert '/usr/bin/pytest-3 -v' in lines[-1] + lines = rpm_eval('%pytest -v', python3_pkgversion="3.6", python3_version="3.6") + # difference RHEL vs Fedora: only pytest-3 binary name + # Changed in rhel-8.7 (https://bugzilla.redhat.com/show_bug.cgi?id=2091462) + if older_rhel87(): + assert '/usr/bin/pytest-3 -v' in lines[-1] + else: + assert '/usr/bin/pytest-3.6 -v' in lines[-1] + + +def test_pypi_source_default_name(): + url = rpm_eval('%pypi_source', + name='foo', version='6')[0] + assert url == 'https://files.pythonhosted.org/packages/source/f/foo/foo-6.tar.gz' + + +def test_pypi_source_default_srcname(): + url = rpm_eval('%pypi_source', + name='python-foo', srcname='foo', version='6')[0] + assert url == 'https://files.pythonhosted.org/packages/source/f/foo/foo-6.tar.gz' + + +def test_pypi_source_default_pypi_name(): + url = rpm_eval('%pypi_source', + name='python-foo', pypi_name='foo', version='6')[0] + assert url == 'https://files.pythonhosted.org/packages/source/f/foo/foo-6.tar.gz' + + +def test_pypi_source_default_name_uppercase(): + url = rpm_eval('%pypi_source', + name='Foo', version='6')[0] + assert url == 'https://files.pythonhosted.org/packages/source/F/Foo/Foo-6.tar.gz' + + +def test_pypi_source_provided_name(): + url = rpm_eval('%pypi_source foo', + name='python-bar', pypi_name='bar', version='6')[0] + assert url == 'https://files.pythonhosted.org/packages/source/f/foo/foo-6.tar.gz' + + +def test_pypi_source_provided_name_version(): + url = rpm_eval('%pypi_source foo 6', + name='python-bar', pypi_name='bar', version='3')[0] + assert url == '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')[0] + assert url == 'https://files.pythonhosted.org/packages/source/f/foo/foo-6.zip' + + +def test_pypi_source_prerelease(): + url = rpm_eval('%pypi_source', + name='python-foo', pypi_name='foo', version='6~b2')[0] + assert url == 'https://files.pythonhosted.org/packages/source/f/foo/foo-6b2.tar.gz' + + +def test_pypi_source_explicit_tilde(): + url = rpm_eval('%pypi_source foo 6~6', + name='python-foo', pypi_name='foo', version='6')[0] + assert url == 'https://files.pythonhosted.org/packages/source/f/foo/foo-6~6.tar.gz' + + +def test_py3_shebang_fix(): + cmd = rpm_eval('%py3_shebang_fix arg1 arg2 arg3')[-1].strip() + # difference RHEL vs Fedora: platform-python + assert cmd == '$pathfix -pni /usr/libexec/platform-python $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)) == '-kas' + + +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('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' + + +def test_py_shebang_fix_custom_python(): + cmd = rpm_eval('%py_shebang_fix arg1 arg2 arg3', __python='/usr/bin/pypy')[-1].strip() + assert cmd == '$pathfix -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(): + lines = rpm_eval('%pycached %{python3_sitearch}/foo*.py') + lib = rpm_eval('%_lib')[0] + 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_36(): + lines = rpm_eval('%pycached /usr/lib/python3.6/site-packages/foo*.py') + assert lines == [ + '/usr/lib/python3.6/site-packages/foo*.py', + '/usr/lib/python3.6/site-packages/__pycache__/foo*.cpython-36{,.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(reason="Not available on RHEL") +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 contains no code, just 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 contains no code, just 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(reason="Not available on RHEL") +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 contains no code, just 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 contains no code, just makes sure the dependencies are installed. + + %files -n python3-setuptools_scm+yaml -f ghost_filelist + """).lstrip().splitlines() + assert lines == expected + +@pytest.mark.skip(reason="Not available on RHEL") +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 contains no code, just 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 contains no code, just makes sure the dependencies are installed. + """).lstrip().splitlines() + assert lines == expected + + +unversioned_macros = pytest.mark.parametrize('macro', [ + '%__python', + '%python', + '%python_version', + pytest.param('%python_version_nodots', marks=pytest.mark.skip(reason="RHEL is different")), + '%python_sitelib', + '%python_sitearch', + pytest.param('%python_platform', marks=pytest.mark.skip(reason="RHEL is different")), + '%python_platform_triplet', + '%python_ext_suffix', + '%py_shebang_fix', + '%py_build', + '%py_build_egg', + '%py_build_wheel', + '%py_install', + pytest.param('%py_install_egg', marks=pytest.mark.skip(reason="RHEL is different")), + pytest.param('%py_install_wheel', marks=pytest.mark.skip(reason="RHEL is different")), #FIXME working macro but not tested +]) + + +@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') + +@pytest.mark.parametrize('macro',[ + '%python_version_nodots', + '%python_platform', +]) +def test_unversioned_python_errors_rhel_specific(macro): + # on rhel - return macro string unchanged and exit with 0 + assert macro in rpm_eval(macro)[-1] + + +@unversioned_macros +def test_unversioned_python_works_when_defined(macro): + macro3 = macro.replace('python', 'python3').replace('py_', 'py3_') + # difference RHEL vs Fedora + assert rpm_eval(macro, __python='/usr/libexec/platform-python') == 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")[0] == "x86_64-linux-gnu" + + +@x86_64_only +def test_ext_suffix(): + assert rpm_eval("%python3_ext_suffix")[0] == f".cpython-{XY}m-x86_64-linux-gnu.so"