diff --git a/src/python/jw/pkg/lib/Distro.py b/src/python/jw/pkg/lib/Distro.py index 04aad14f..147583ec 100644 --- a/src/python/jw/pkg/lib/Distro.py +++ b/src/python/jw/pkg/lib/Distro.py @@ -49,7 +49,7 @@ class Distro(abc.ABC): 'name', 'codename', 'gnu-triplet', - 'os-cascade', + 'cascade', 'os-release', 'pkg-ext', ] @@ -210,6 +210,12 @@ class Distro(abc.ABC): ) return self.__os_release_str + # -- expand_macros() looks up macro names via getattr with '-' turned + # into '_', so 'os-release' needs this alias + @property + def os_release(self) -> str: + return self.os_release_str + @cached_property def name(self) -> str: return self.os_release_field('NAME') @@ -284,6 +290,8 @@ class Distro(abc.ABC): @classmethod def macros(cls) -> list[str]: + # -- The doubled percent is not a typo: the macros are embedded + # in an argparse help string, where %% escapes to a literal % return ['%%{' + name + '}' for name in cls.macro_names] def expand_macros(self, fmt: str | Iterable[str]) -> str | list[str]: diff --git a/test/integration/jw-pkg/help/test-expected.txt b/test/integration/jw-pkg/help/test-expected.txt index 5bd788a1..2a08dea0 100644 --- a/test/integration/jw-pkg/help/test-expected.txt +++ b/test/integration/jw-pkg/help/test-expected.txt @@ -161,7 +161,7 @@ Retrieve information about target platform options: -h, --help show this help message and exit --format FORMAT Format string, expanding macros %{os}, %{id}, %{name}, - %{codename}, %{gnu-triplet}, %{os-cascade}, %{os-release}, + %{codename}, %{gnu-triplet}, %{cascade}, %{os-release}, %{pkg-ext} (default: %{cascade}) ============= Running: jw-pkg.py -t ../../../.. --log-level info posix --help usage: jw-pkg.py posix [-h] ... diff --git a/test/unit/python/jw/pkg/lib/Distro/Makefile b/test/unit/python/jw/pkg/lib/Distro/Makefile new file mode 100644 index 00000000..de693650 --- /dev/null +++ b/test/unit/python/jw/pkg/lib/Distro/Makefile @@ -0,0 +1,7 @@ +TOPDIR = ../../../../../../.. + +include $(TOPDIR)/make/proj.mk +include $(JWBDIR)/make/py-run.mk + +all: +test: run diff --git a/test/unit/python/jw/pkg/lib/Distro/test.py b/test/unit/python/jw/pkg/lib/Distro/test.py new file mode 100644 index 00000000..dbde4818 --- /dev/null +++ b/test/unit/python/jw/pkg/lib/Distro/test.py @@ -0,0 +1,153 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Iterable, cast, override + +from jw.pkg.lib.Distro import Distro + +if TYPE_CHECKING: + from jw.pkg.lib.ExecContext import ExecContext + from jw.pkg.lib.Package import Package + +OS_RELEASE_UBUNTU = """\ +NAME="Ubuntu" +VERSION="24.04.2 LTS (Noble Numbat)" +ID=ubuntu +ID_LIKE=debian +PRETTY_NAME="Ubuntu 24.04.2 LTS" +VERSION_ID="24.04" +VERSION_CODENAME=noble +HOME_URL="https://www.ubuntu.com/" +""" + +OS_RELEASE_SUSE = """\ +NAME="openSUSE Tumbleweed" +# VERSION="20260901" +ID=opensuse-tumbleweed +ID_LIKE="opensuse suse" +PRETTY_NAME="openSUSE Tumbleweed" +VERSION_ID="20260901" +HOME_URL="https://www.opensuse.org/" +""" + +class DummyDistro(Distro): + + @override + async def _ref(self) -> None: + pass + + @override + async def _dup(self, download_only: bool) -> None: + pass + + @override + async def _reboot_required(self, verbose: bool) -> bool: + return False + + @override + async def _select_by_name(self, names: Iterable[str]) -> Iterable[Package]: + return [] + + @override + async def _install(self, names: Iterable[str], only_update: bool) -> None: + pass + + @override + async def _delete(self, names: Iterable[str]) -> None: + pass + + @override + async def _pkg_files(self, name: str) -> Iterable[str]: + return [] + +def distro(os_release_str: str) -> Distro: + # -- The execution context is stored but not used by the properties + # under test, so a dummy object is sufficient + return DummyDistro( + ec = cast('ExecContext', object()), + id = Distro.parse_os_release_field_id(os_release_str), + os_release_str = os_release_str + ) + +# -- parse_os_release_field -- + +assert Distro.parse_os_release_field('ID', OS_RELEASE_UBUNTU) == 'ubuntu' +assert Distro.parse_os_release_field('VERSION_CODENAME', OS_RELEASE_UBUNTU) == \ + 'noble' +# Quoted values are de-quoted +assert Distro.parse_os_release_field('NAME', OS_RELEASE_UBUNTU) == 'Ubuntu' +# ID must not match ID_LIKE +assert Distro.parse_os_release_field('ID_LIKE', OS_RELEASE_UBUNTU) == 'debian' + +try: + Distro.parse_os_release_field('NO_SUCH_KEY', OS_RELEASE_UBUNTU) + assert False, 'Should have raised' +except Exception: + pass + +# -- parse_os_release_field_id -- + +assert Distro.parse_os_release_field_id(OS_RELEASE_UBUNTU) == 'ubuntu' +assert Distro.parse_os_release_field_id(OS_RELEASE_SUSE) == 'suse' + +# -- Properties -- + +d = distro(OS_RELEASE_UBUNTU) +assert d.id == 'ubuntu' +assert d.name == 'Ubuntu' +assert d.codename == 'noble' +assert d.os == 'ubuntu-noble' +assert d.pkg_ext == 'debian' +assert d.os_cascade == ['os', 'linux', 'pkg-debian', 'pm-apt', 'ubuntu', 'ubuntu-noble'] +assert d.cascade == 'os linux pkg-debian pm-apt ubuntu ubuntu-noble' + +s = distro(OS_RELEASE_SUSE) +assert s.id == 'suse' +assert s.codename == 'tumbleweed' +assert s.os == 'suse-tumbleweed' +assert s.pkg_ext == 'rpm' +assert s.os_cascade == [ + 'os', 'linux', 'pkg-rpm', 'pm-zypper', 'suse', 'suse-tumbleweed' +] + +# A codename with a version number cascades down to its prefixes +v = distro('ID=fake\nVERSION_CODENAME=1.2.3\n') +assert v.os_cascade == ['os', 'fake-1.2', 'fake-1', 'fake', 'fake-1.2.3'] + +# -- expand_macros -- + +assert d.expand_macros('%{id}') == 'ubuntu' +assert d.expand_macros('%{name}') == 'Ubuntu' +assert d.expand_macros('%{os}') == 'ubuntu-noble' +assert d.expand_macros('%{codename}') == 'noble' +assert d.expand_macros('%{pkg-ext}') == 'debian' +assert d.expand_macros('%{cascade}') == d.cascade +# %{os-release} expands to the whole /etc/os-release content +assert d.expand_macros('%{os-release}') == d.os_release_str +assert d.expand_macros('%{os-release}') == OS_RELEASE_UBUNTU +assert d.os_release == d.os_release_str + +# A mix of macros and literal text +assert d.expand_macros('running on %{id} (%{codename}) now') == \ + 'running on ubuntu (noble) now' + +# List input expands entry by entry +assert d.expand_macros(['%{id}', '%{name}']) == ['ubuntu', 'Ubuntu'] + +# An unknown macro raises +try: + d.expand_macros('%{no-such-macro}') + assert False, 'Should have raised' +except Exception: + pass + +# -- macros -- + +# -- The doubled percent escapes to a single one when the macros are +# embedded in an argparse help string +assert Distro.macros() == ['%%{' + name + '}' for name in Distro.macro_names] +for macro in Distro.macros(): + # -- Every advertised macro must expand without error once the + # argparse help escaping has been undone + d.expand_macros(macro.replace('%%', '%')) + +print('All Distro tests passed')