Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -85,6 +87,7 @@ jobs:
python3-ldap \
python3-pip \
python3-pytest \
python3-setuptools \
python3-PyYAML \
python3-tomli

Expand Down Expand Up @@ -151,6 +154,7 @@ jobs:
python3-pip \
python3-pytest \
python3-requests \
python3-setuptools \
python3-tomli \
python3-yaml \
python3-venv
Expand Down Expand Up @@ -195,6 +199,7 @@ jobs:
python3 \
python3-pip \
python3-pytest \
python3-setuptools \
python3-Flask \
python3-PyJWT \
python3-ldap \
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ tests = [
"python-ldap",
"PyYAML",
"requests",
"setuptools",
]

[tool.pytest.ini_options]
Expand Down
279 changes: 279 additions & 0 deletions src/build/rfl/tests/test_setup.py
Original file line number Diff line number Diff line change
@@ -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"])
Loading