lib.Distro: Fix os-release and cascade macros
All checks were successful
CI / Packaging - Kali Linux (pull_request) Successful in 4m34s
CI / Packaging - OpenSUSE Tumbleweed (pull_request) Successful in 4m30s
CI / Packaging test (pull_request) Successful in 0s
CI / Packaging - Kali Linux (push) Successful in 4m30s
CI / Packaging - OpenSUSE Tumbleweed (push) Successful in 4m16s
CI / Packaging test (push) Successful in 0s

expand_macros() looks up each %{name} via getattr with '-' turned into '_',
but macro_names advertises two names that don't resolve: 'os-release' has
no corresponding attribute, and 'os-cascade' resolves to the list property
os_cascade, which str.replace() rejects. Both macros are listed in the
platform info help output, yet always fail when used in a format string.

The space-joined cascade string is the cascade property, and 'cascade' is
the macro name the make recipes and scripts already use, together with the
--format default in cmds/platform/CmdInfo.py.

Before the App.distro_* properties moved to Distro, the advertised macro
names were derived from the distro_* attribute names, so the advertised
macro was %{cascade}; the hand-written macro_names replaced it with the
list attribute's name.

- Add the missing os_release attribute as an alias of os_release_str
- Advertise the 'cascade' macro in macro_names instead of 'os-cascade',
  leaving os_cascade as the purely programmatic list
- Update the platform info help expected output accordingly
- Add unit tests for the os-release field parsers, the derived properties,
  and the expansion of every advertised macro

Signed-off-by: Jan Lindemann <jan@janware.com>
Assisted-by: unsloth/Qwen3.8-27B-GGUF:Q4_K_M with pi.dev v0.85.1
This commit is contained in:
Jan Lindemann 2026-09-08 22:20:07 +02:00
commit 13e6ae8807
Signed by: Jan Lindemann
GPG key ID: 3750640C9E25DD61
4 changed files with 170 additions and 2 deletions

View file

@ -0,0 +1,7 @@
TOPDIR = ../../../../../../..
include $(TOPDIR)/make/proj.mk
include $(JWBDIR)/make/py-run.mk
all:
test: run

View file

@ -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')