From eb172a60828114225e6be4c6d2f7f26e0ff32543 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Palancher?= Date: Thu, 2 Jul 2026 09:06:22 +0200 Subject: [PATCH 1/3] tests(build): add tests to cover setup converter --- src/build/rfl/tests/test_setup.py | 279 ++++++++++++++++++++++++++++++ 1 file changed, 279 insertions(+) create mode 100644 src/build/rfl/tests/test_setup.py diff --git a/src/build/rfl/tests/test_setup.py b/src/build/rfl/tests/test_setup.py new file mode 100644 index 0000000..8afa86e --- /dev/null +++ b/src/build/rfl/tests/test_setup.py @@ -0,0 +1,279 @@ +# Copyright (c) 2026 Rackslab +# +# This file is part of RFL. +# +# SPDX-License-Identifier: LGPL-3.0-or-later + +import os +import tempfile +import textwrap +import unittest +from contextlib import contextmanager +from pathlib import Path +from unittest.mock import patch + +from setuptools import find_packages + + +def _setup_script_path(): + return Path(__file__).resolve().parents[1] / "build" / "scripts" / "setup" + + +def _write_pyproject(tmpdir, content): + Path(tmpdir).joinpath("pyproject.toml").write_text(content, encoding="utf-8") + + +def _run_setup_script(): + path = _setup_script_path() + code = path.read_text(encoding="utf-8") + exec(compile(code, str(path), "exec"), {"__name__": "__main__"}) + + +def _minimal_pyproject(extra="", license_line='license = "LGPL-3.0-or-later"'): + content = textwrap.dedent( + f"""\ + [project] + name = "test-pkg" + version = "1.0.0" + {license_line} + authors = [ + {{name = "Test Author", email = "test@example.com"}}, + ] + """ + ) + if extra: + content += textwrap.dedent(extra) + return content + + +@contextmanager +def _project_dir(pyproject_content, layout=None): + previous_cwd = os.getcwd() + with tempfile.TemporaryDirectory() as tmpdir: + _write_pyproject(tmpdir, pyproject_content) + if layout: + layout(Path(tmpdir)) + os.chdir(tmpdir) + try: + yield tmpdir + finally: + os.chdir(previous_cwd) + + +class TestSetupScript(unittest.TestCase): + @patch("sys.exit") + @patch("setuptools.setup") + def test_missing_project_section_exits(self, mock_setup, mock_exit): + mock_exit.side_effect = SystemExit + pyproject = textwrap.dedent( + """\ + [build-system] + requires = ['setuptools'] + """ + ) + with _project_dir(pyproject): + with self.assertRaises(SystemExit): + _run_setup_script() + + mock_exit.assert_called_once_with(0) + mock_setup.assert_not_called() + + @patch("sys.exit") + @patch("setuptools.setup") + def test_minimal_project_metadata(self, mock_setup, mock_exit): + with _project_dir(_minimal_pyproject()): + _run_setup_script() + + mock_exit.assert_not_called() + mock_setup.assert_called_once_with( + name="test-pkg", + version="1.0.0", + author="Test Author", + author_email="test@example.com", + platforms=["GNU/Linux"], + ) + + @patch("sys.exit") + @patch("setuptools.setup") + def test_console_scripts_entry_points(self, mock_setup, mock_exit): + extra = """ + [project.scripts] + my-cmd = "mypkg.cli:main" + """ + with _project_dir(_minimal_pyproject(extra)): + _run_setup_script() + + mock_setup.assert_called_once() + kwargs = mock_setup.call_args[1] + self.assertEqual( + kwargs["entry_points"], + {"console_scripts": ["my-cmd=mypkg.cli:main"]}, + ) + + @patch("sys.exit") + @patch("setuptools.setup") + def test_namespace_packages_explicit_list(self, mock_setup, mock_exit): + extra = """ + [tool.setuptools.packages.find] + include = ["rfl.build*"] + """ + + def layout(root): + rfl_build = root / "rfl" / "build" + rfl_build.mkdir(parents=True) + rfl_build.joinpath("__init__.py").write_text("", encoding="utf-8") + + with _project_dir(_minimal_pyproject(extra), layout=layout): + _run_setup_script() + + mock_setup.assert_called_once() + self.assertEqual(mock_setup.call_args[1]["packages"], ["rfl.build"]) + + @patch("sys.exit") + @patch("setuptools.setup") + def test_autofind_packages(self, mock_setup, mock_exit): + extra = """ + [tool.setuptools.packages.find] + include = ["mypkg.*"] + """ + + def layout(root): + mypkg = root / "mypkg" + mypkg.mkdir() + mypkg.joinpath("__init__.py").write_text("", encoding="utf-8") + + with _project_dir(_minimal_pyproject(extra), layout=layout): + _run_setup_script() + expected_packages = find_packages() + + mock_setup.assert_called_once() + self.assertEqual( + mock_setup.call_args[1]["packages"], + expected_packages, + ) + + @patch("sys.exit") + @patch("setuptools.setup") + def test_explicit_packages_without_find(self, mock_setup, mock_exit): + extra = """ + [tool.setuptools] + packages = ["pkg.a", "pkg.b"] + """ + with _project_dir(_minimal_pyproject(extra)): + _run_setup_script() + + mock_setup.assert_called_once() + self.assertEqual( + mock_setup.call_args[1]["packages"], + ["pkg.a", "pkg.b"], + ) + + @patch("sys.exit") + @patch("setuptools.setup") + def test_package_data(self, mock_setup, mock_exit): + extra = """ + [tool.setuptools.package-data] + "pkg.data" = ["*.json"] + """ + with _project_dir(_minimal_pyproject(extra)): + _run_setup_script() + + mock_setup.assert_called_once() + kwargs = mock_setup.call_args[1] + self.assertEqual(kwargs["package_data"], {"pkg.data": ["*.json"]}) + self.assertTrue(kwargs["include_package_data"]) + + @patch("sys.exit") + @patch("setuptools.setup") + def test_install_requires(self, mock_setup, mock_exit): + extra = """ + dependencies = [ + "dep-a", + "dep-b", + ] + """ + with _project_dir(_minimal_pyproject(extra)): + _run_setup_script() + + mock_setup.assert_called_once() + self.assertEqual( + mock_setup.call_args[1]["install_requires"], + ["dep-a", "dep-b"], + ) + + @patch("sys.exit") + @patch("setuptools.setup") + def test_extras_require(self, mock_setup, mock_exit): + extra = """ + [project.optional-dependencies] + dev = ["pytest"] + docs = ["sphinx"] + """ + with _project_dir(_minimal_pyproject(extra)): + _run_setup_script() + + mock_setup.assert_called_once() + self.assertEqual( + mock_setup.call_args[1]["extras_require"], + {"dev": ["pytest"], "docs": ["sphinx"]}, + ) + + @patch("sys.exit") + @patch("setuptools.setup") + def test_license_text(self, mock_setup, mock_exit): + with _project_dir( + _minimal_pyproject(license_line='license = { text = "MIT" }') + ): + _run_setup_script() + + mock_setup.assert_called_once() + self.assertEqual(mock_setup.call_args[1]["license"], "MIT") + + @patch("sys.exit") + @patch("setuptools.setup") + def test_license_file(self, mock_setup, mock_exit): + with _project_dir( + _minimal_pyproject(license_line='license = { file = "LICENSE" }') + ): + _run_setup_script() + + mock_setup.assert_called_once() + self.assertEqual( + mock_setup.call_args[1]["license_files"], + ["LICENSE"], + ) + + @patch("sys.exit") + @patch("setuptools.setup") + def test_homepage_url(self, mock_setup, mock_exit): + extra = """ + [project.urls] + Homepage = "https://example.com" + """ + with _project_dir(_minimal_pyproject(extra)): + _run_setup_script() + + mock_setup.assert_called_once() + self.assertEqual( + mock_setup.call_args[1]["url"], + "https://example.com", + ) + + @patch("sys.exit") + @patch("setuptools.setup") + def test_skip_bare_include_pattern(self, mock_setup, mock_exit): + extra = """ + [tool.setuptools.packages.find] + include = ["rfl", "rfl.build*"] + """ + + def layout(root): + rfl_build = root / "rfl" / "build" + rfl_build.mkdir(parents=True) + rfl_build.joinpath("__init__.py").write_text("", encoding="utf-8") + + with _project_dir(_minimal_pyproject(extra), layout=layout): + _run_setup_script() + + mock_setup.assert_called_once() + self.assertEqual(mock_setup.call_args[1]["packages"], ["rfl.build"]) From 43f322a7452385961fe06aac13a2f4af8adecad6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Palancher?= Date: Thu, 2 Jul 2026 09:26:32 +0200 Subject: [PATCH 2/3] chore: add setuptools in tests extra deps --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index bb16e18..8626a61 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,6 +21,7 @@ tests = [ "python-ldap", "PyYAML", "requests", + "setuptools", ] [tool.pytest.ini_options] From 694f52c2a57ca2d8489f63185d1cbe327edd652a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Palancher?= Date: Thu, 2 Jul 2026 09:29:17 +0200 Subject: [PATCH 3/3] ci: install setuptools to run tests --- .github/workflows/ci.yaml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index c824562..4d5fbe2 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -25,11 +25,10 @@ jobs: with: python-version: ${{ matrix.python-version }} - - name: Install tests dependencies + - name: Install system dependencies run: | sudo apt-get install -y build-essential python3-dev libldap2-dev libsasl2-dev python -m pip install --upgrade pip - pip install pytest # The version of setuptools in python 3.8 environment does not support PEP 639 # license metadata format. Patch pyproject.toml for this version to restore old @@ -39,6 +38,9 @@ jobs: run: | sed -i 's/^license = \(".*"\)/license = {text = \1}/' pyproject.toml src/*/pyproject.toml + - name: Install tests dependencies + run: pip install ".[tests]" + - name: Install application run: | pip install src/core "src/authentication[all]" src/build src/log src/permissions src/settings src/web @@ -85,6 +87,7 @@ jobs: python3-ldap \ python3-pip \ python3-pytest \ + python3-setuptools \ python3-PyYAML \ python3-tomli @@ -151,6 +154,7 @@ jobs: python3-pip \ python3-pytest \ python3-requests \ + python3-setuptools \ python3-tomli \ python3-yaml \ python3-venv @@ -195,6 +199,7 @@ jobs: python3 \ python3-pip \ python3-pytest \ + python3-setuptools \ python3-Flask \ python3-PyJWT \ python3-ldap \