jw-pkg packages install: Add -x/--exact option #159
22 changed files with 1301 additions and 50 deletions
|
|
@ -26,6 +26,14 @@ class CmdInstall(Cmd): # export
|
|||
action = 'store_true',
|
||||
help = "Only update the listed packages, don't install them",
|
||||
)
|
||||
parser.add_argument(
|
||||
'-x',
|
||||
'--exact',
|
||||
default = False,
|
||||
action = 'store_true',
|
||||
help = "Install local files directly, without resolving "
|
||||
"dependencies",
|
||||
)
|
||||
parser.add_argument(
|
||||
'-F',
|
||||
'--fixed-strings',
|
||||
|
|
@ -38,4 +46,8 @@ class CmdInstall(Cmd): # export
|
|||
names = (
|
||||
args.names if args.fixed_strings else self.distro.expand_macros(args.names)
|
||||
)
|
||||
return await self.distro.install(names, only_update = args.only_update)
|
||||
return await self.distro.install(
|
||||
names,
|
||||
only_update = args.only_update,
|
||||
install_deps = not (args.only_update or args.exact),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -401,17 +401,27 @@ class Distro(abc.ABC):
|
|||
async def _install(self, names: Iterable[str], only_update: bool) -> None:
|
||||
pass
|
||||
|
||||
# Default implementation assumes package manager can handle local files.
|
||||
# Not true for all distros. Override if Distro knows better.
|
||||
# The default route passes local files to the package manager, which
|
||||
# resolves their dependencies. The direct route (install_deps = False)
|
||||
# is not available here; override in descendants that offer it.
|
||||
async def _install_local_files(
|
||||
self, paths: Iterable[str], only_update: bool
|
||||
self, paths: Iterable[str], only_update: bool, install_deps: bool
|
||||
) -> None:
|
||||
assert not (only_update and install_deps)
|
||||
if not install_deps:
|
||||
raise NotImplementedError(
|
||||
f'Installing local files without dependency resolution '
|
||||
f'is not supported on {self.os}'
|
||||
)
|
||||
await self._install(paths, only_update = only_update)
|
||||
|
||||
# Download first and then install. Override if Distro knows better.
|
||||
async def _install_urls(self, urls: Iterable[str], only_update: bool) -> None:
|
||||
async def _install_urls(
|
||||
self, urls: Iterable[str], only_update: bool, install_deps: bool
|
||||
) -> None:
|
||||
from .util import copy
|
||||
|
||||
assert not (only_update and install_deps)
|
||||
tmp: str | None = None
|
||||
try:
|
||||
tmp = await self.__exec_context.mktemp(
|
||||
|
|
@ -424,7 +434,9 @@ class Distro(abc.ABC):
|
|||
raise paths
|
||||
if isinstance(paths, str):
|
||||
paths = [paths]
|
||||
await self._install_local_files(paths, only_update = only_update)
|
||||
await self._install_local_files(
|
||||
paths, only_update = only_update, install_deps = install_deps
|
||||
)
|
||||
finally:
|
||||
if tmp is not None:
|
||||
await self.__exec_context.erase(tmp)
|
||||
|
|
@ -434,8 +446,10 @@ class Distro(abc.ABC):
|
|||
# - Pass names to package manager
|
||||
# Override if Distro knows better.
|
||||
async def _install_urls_and_names(
|
||||
self, packages: Iterable[str], only_update: bool
|
||||
self, packages: Iterable[str], only_update: bool, install_deps: bool
|
||||
) -> None:
|
||||
|
||||
assert not (only_update and install_deps)
|
||||
urls: list[str] = []
|
||||
names: list[str] = []
|
||||
for package in packages:
|
||||
|
|
@ -447,15 +461,25 @@ class Distro(abc.ABC):
|
|||
continue
|
||||
names.append(package)
|
||||
if urls:
|
||||
await self._install_urls(urls, only_update = only_update)
|
||||
await self._install_urls(
|
||||
urls, only_update = only_update, install_deps = install_deps
|
||||
)
|
||||
if names:
|
||||
await self._install(names, only_update = only_update)
|
||||
|
||||
async def install(self, names: Collection[str], only_update: bool = False) -> None:
|
||||
async def install(
|
||||
self,
|
||||
names: Collection[str],
|
||||
only_update: bool,
|
||||
install_deps: bool = True,
|
||||
) -> None:
|
||||
assert not (only_update and install_deps)
|
||||
if not names:
|
||||
log(WARNING, 'No packages specified for installation')
|
||||
return
|
||||
await self._install_urls_and_names(names, only_update = only_update)
|
||||
await self._install_urls_and_names(
|
||||
names, only_update = only_update, install_deps = install_deps
|
||||
)
|
||||
|
||||
# -- delete
|
||||
|
||||
|
|
|
|||
|
|
@ -8,19 +8,25 @@ meta_tags = [
|
|||
'packager',
|
||||
'url',
|
||||
'maintainer',
|
||||
'version',
|
||||
]
|
||||
|
||||
class Package:
|
||||
"""A package as reported by a package manager. The version is the
|
||||
full version string as the PM reports it, for instance
|
||||
version-release for rpm and the single version for dpkg."""
|
||||
|
||||
name: str
|
||||
vendor: str | None = None
|
||||
packager: str | None = None
|
||||
url: str | None = None
|
||||
maintainer: str | None = None
|
||||
version: str | None = None
|
||||
|
||||
@classmethod
|
||||
def parse_spec_str(cls, spec: str, delimiter: str = '|') -> 'Package':
|
||||
tags = spec.split(delimiter)
|
||||
if len(tags) != 5:
|
||||
if len(tags) != len(meta_tags):
|
||||
raise ValueError(f'Invalid package spec string "{spec}"')
|
||||
return cls(
|
||||
name = tags[0],
|
||||
|
|
@ -28,6 +34,7 @@ class Package:
|
|||
packager = tags[2],
|
||||
url = tags[3],
|
||||
maintainer = tags[4],
|
||||
version = tags[5],
|
||||
)
|
||||
|
||||
@classmethod
|
||||
|
|
@ -51,12 +58,14 @@ class Package:
|
|||
packager: str | None = None,
|
||||
url: str | None = None,
|
||||
maintainer: str | None = None,
|
||||
version: str | None = None,
|
||||
):
|
||||
self.name = name
|
||||
self.vendor = vendor
|
||||
self.packager = packager
|
||||
self.url = url
|
||||
self.maintainer = maintainer
|
||||
self.version = version
|
||||
|
||||
@override
|
||||
def __repr__(self) -> str:
|
||||
|
|
|
|||
|
|
@ -6,7 +6,13 @@ from typing import TYPE_CHECKING, Any, override
|
|||
|
||||
from ...Distro import Distro as Base
|
||||
from ...log import NOTICE, log
|
||||
from ...pm.dpkg import list_files, query_packages, run_dpkg, verify_packages
|
||||
from ...pm.dpkg import (
|
||||
install_packages,
|
||||
list_files,
|
||||
query_packages,
|
||||
run_dpkg,
|
||||
verify_packages,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Iterable
|
||||
|
|
@ -81,6 +87,17 @@ class Distro(Base):
|
|||
args.extend(names)
|
||||
await self.apt_get(args)
|
||||
|
||||
@override
|
||||
async def _install_local_files(
|
||||
self, paths: Iterable[str], only_update: bool, install_deps: bool
|
||||
) -> None:
|
||||
if install_deps:
|
||||
await self._install(paths, only_update = False)
|
||||
return
|
||||
await install_packages(
|
||||
paths, ec = self.ctx, force = True, only_update = only_update
|
||||
)
|
||||
|
||||
@override
|
||||
async def _delete(self, names: Iterable[str]) -> None:
|
||||
await self.dpkg(['-P', *names], sudo = True)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,13 @@ from __future__ import annotations
|
|||
from typing import TYPE_CHECKING, override
|
||||
|
||||
from ...Distro import Distro as Base
|
||||
from ...pm.rpm import list_files, query_packages, run_rpm, verify_packages
|
||||
from ...pm.rpm import (
|
||||
install_packages,
|
||||
list_files,
|
||||
query_packages,
|
||||
run_rpm,
|
||||
verify_packages,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any, Collection, Iterable
|
||||
|
|
@ -66,6 +72,17 @@ class Distro(Base):
|
|||
args.extend(names)
|
||||
await self.yum(args)
|
||||
|
||||
@override
|
||||
async def _install_local_files(
|
||||
self, paths: Iterable[str], only_update: bool, install_deps: bool
|
||||
) -> None:
|
||||
if install_deps:
|
||||
await self._install(paths, only_update = False)
|
||||
return
|
||||
await install_packages(
|
||||
paths, ec = self.ctx, force = True, only_update = only_update
|
||||
)
|
||||
|
||||
@override
|
||||
async def _delete(self, names: Iterable[str]) -> None:
|
||||
await self.rpm(['-e', *names], sudo = True)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,13 @@ from __future__ import annotations
|
|||
from typing import TYPE_CHECKING, Any, override
|
||||
|
||||
from ...Distro import Distro as Base
|
||||
from ...pm.rpm import list_files, query_packages, run_rpm, verify_packages
|
||||
from ...pm.rpm import (
|
||||
install_packages,
|
||||
list_files,
|
||||
query_packages,
|
||||
run_rpm,
|
||||
verify_packages,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Collection
|
||||
|
|
@ -87,9 +93,14 @@ class Distro(Base):
|
|||
|
||||
@override
|
||||
async def _install_local_files(
|
||||
self, paths: Iterable[str], only_update: bool
|
||||
self, paths: Iterable[str], only_update: bool, install_deps: bool
|
||||
) -> None:
|
||||
await self.rpm(['-U', '--reinstall', *paths])
|
||||
if install_deps:
|
||||
await self._install(paths, only_update = False)
|
||||
return
|
||||
await install_packages(
|
||||
paths, ec = self.ctx, force = True, only_update = only_update
|
||||
)
|
||||
|
||||
@override
|
||||
async def _delete(self, names: Iterable[str]) -> None:
|
||||
|
|
|
|||
|
|
@ -17,11 +17,12 @@ def meta_map() -> dict[str, str]:
|
|||
if _meta_map is None:
|
||||
_meta_map = Package.order_tags(
|
||||
{
|
||||
'name': 'binary:Package',
|
||||
'name': '${binary:Package}',
|
||||
'vendor': None, # deb doesn't have vendor field
|
||||
'packager': None, # -- packager --
|
||||
'url': 'Homepage',
|
||||
'maintainer': 'Maintainer',
|
||||
'url': '${Homepage}',
|
||||
'maintainer': '${Maintainer}',
|
||||
'version': '${Version}',
|
||||
}
|
||||
)
|
||||
return _meta_map
|
||||
|
|
@ -33,7 +34,7 @@ async def _run(
|
|||
**kwargs: Any,
|
||||
) -> str:
|
||||
return (
|
||||
await run_sudo(cmd) if sudo else await
|
||||
await run_sudo(cmd, ec = ec, **kwargs) if sudo else await
|
||||
run_cmd(cmd, ec = ec, cmd_input = InputMode.NonInteractive, **kwargs)
|
||||
).stdout_str
|
||||
|
||||
|
|
@ -50,20 +51,23 @@ async def run_dpkg( # export
|
|||
async def run_dpkg_query( # export
|
||||
args: list[str],
|
||||
sudo: bool = False,
|
||||
ec: ExecContext | None = None
|
||||
ec: ExecContext | None = None,
|
||||
**kwargs: Any,
|
||||
) -> str:
|
||||
cmd = ['/usr/bin/dpkg-query']
|
||||
cmd.extend(args)
|
||||
return await _run(cmd, sudo, ec)
|
||||
return await _run(cmd, sudo, ec, **kwargs)
|
||||
|
||||
async def query_packages(names: Iterable[str] = [],
|
||||
ec: ExecContext | None = None) -> Iterable[Package]:
|
||||
fmt_str = (
|
||||
'|'.join([(f'${{{tag}}}' if tag else '')
|
||||
for tag in meta_map().values()]) + r'\n'
|
||||
)
|
||||
async def query_packages(
|
||||
names: Iterable[str] = [],
|
||||
ec: ExecContext | None = None,
|
||||
throw: bool = True
|
||||
) -> Iterable[Package]:
|
||||
fmt_str = '|'.join(tag or '' for tag in meta_map().values()) + r'\n'
|
||||
# dpkg-query -W -f='${binary:Package}|${Maintainer}| ... \n'
|
||||
specs = await run_dpkg_query(['-W', '-f=' + fmt_str, *names], sudo = False, ec = ec)
|
||||
specs = await run_dpkg_query(
|
||||
['-W', '-f=' + fmt_str, *names], sudo = False, throw = throw, ec = ec
|
||||
)
|
||||
return cast('Iterable[Package]', Package.parse_specs_str(specs))
|
||||
|
||||
async def list_files(
|
||||
|
|
@ -90,6 +94,111 @@ async def list_files(
|
|||
file_list_str = await run_dpkg(['-L', *names], sudo = False, ec = ec)
|
||||
return file_list_str.splitlines()
|
||||
|
||||
async def _file_name_version(
|
||||
path: str,
|
||||
ec: ExecContext | None = None,
|
||||
) -> tuple[str, str]:
|
||||
"""Return the (name, version) pair of a local deb file."""
|
||||
out = await run_dpkg(['-I', path], sudo = False, throw = True, ec = ec)
|
||||
name: str | None = None
|
||||
version: str | None = None
|
||||
for line in out.splitlines():
|
||||
line = line.strip()
|
||||
if line.startswith('Package: '):
|
||||
name = line[len('Package: '):].strip()
|
||||
elif line.startswith('Version: '):
|
||||
version = line[len('Version: '):].strip()
|
||||
if name is None or version is None:
|
||||
raise Exception(f'cannot read the package name and version of {path}')
|
||||
return name, version
|
||||
|
||||
async def _simulate_install(
|
||||
files: list[str],
|
||||
expected: set[str],
|
||||
ec: ExecContext | None = None,
|
||||
) -> None:
|
||||
"""Gate a direct dpkg install on an apt simulation.
|
||||
|
||||
dpkg -i leaves a half-configured package when a dependency is
|
||||
missing, instead of aborting the transaction cleanly, so the
|
||||
install is simulated first and refused when the simulation
|
||||
would install or remove anything but the expected packages.
|
||||
"""
|
||||
result = await run_sudo(
|
||||
['/usr/bin/apt-get', '-s', '--no-install-recommends', 'install', *files],
|
||||
ec = ec,
|
||||
cmd_input = InputMode.NonInteractive,
|
||||
throw = False,
|
||||
)
|
||||
out = result.stdout_str
|
||||
if result.status != 0:
|
||||
raise Exception(f'apt simulation of the direct install failed:\n{out}')
|
||||
inst: list[str] = []
|
||||
remv: list[str] = []
|
||||
for line in out.splitlines():
|
||||
parts = line.split()
|
||||
if parts and parts[0] == 'Inst':
|
||||
inst.append(parts[1])
|
||||
elif parts and parts[0] == 'Remv':
|
||||
remv.append(parts[1])
|
||||
pulled = [pkg for pkg in inst if pkg not in expected]
|
||||
if pulled or remv:
|
||||
raise Exception(f'apt simulation would install {pulled} or remove {remv}')
|
||||
|
||||
async def install_packages(
|
||||
paths: Iterable[str],
|
||||
ec: ExecContext | None = None,
|
||||
force: bool = False,
|
||||
only_update: bool = False,
|
||||
) -> None:
|
||||
"""Install local deb files.
|
||||
|
||||
Every install is gated on an apt simulation, which refuses the
|
||||
transaction when it would install or remove anything but the
|
||||
given files' own packages; see _simulate_install().
|
||||
|
||||
Files are classified against the installed state of their
|
||||
packages: files for packages that are not installed are
|
||||
installed fresh, files with another version installed are
|
||||
upgraded, and files whose version is already installed are
|
||||
reinstalled with -i --force-reinstall when force is set and
|
||||
skipped otherwise. The upgrades run before the reinstalls.
|
||||
|
||||
With only_update, a file for a package that is not installed
|
||||
raises before anything is installed.
|
||||
"""
|
||||
paths = list(paths)
|
||||
if not paths:
|
||||
return
|
||||
update: list[str] = []
|
||||
reinstall: list[str] = []
|
||||
not_installed: list[str] = []
|
||||
names: dict[str, str] = {}
|
||||
for path in paths:
|
||||
name, version = await _file_name_version(path, ec = ec)
|
||||
names[path] = name
|
||||
pkgs = list(await query_packages([name], ec = ec, throw = False))
|
||||
if not pkgs:
|
||||
(not_installed if only_update else update).append(path)
|
||||
continue
|
||||
if version in {p.version for p in pkgs if p.version}:
|
||||
reinstall.append(path)
|
||||
continue
|
||||
update.append(path)
|
||||
if not_installed:
|
||||
raise Exception(
|
||||
'only_update: package not installed for ' +
|
||||
'\n'.join(f' {p}' for p in not_installed)
|
||||
)
|
||||
selected = [*update, *reinstall] if force else update
|
||||
if not selected:
|
||||
return
|
||||
await _simulate_install(selected, {names[path] for path in selected}, ec = ec)
|
||||
if update:
|
||||
await run_dpkg(['-i', *update], sudo = True, ec = ec)
|
||||
if force and reinstall:
|
||||
await run_dpkg(['-i', '--force-reinstall', *reinstall], sudo = True, ec = ec)
|
||||
|
||||
def _parse_verify_output(
|
||||
pkg: str,
|
||||
output: str,
|
||||
|
|
|
|||
|
|
@ -19,11 +19,13 @@ def meta_map() -> dict[str, str]:
|
|||
if _meta_map is None:
|
||||
_meta_map = Package.order_tags(
|
||||
{
|
||||
'name': 'Name',
|
||||
'vendor': 'Vendor',
|
||||
'packager': 'Packager',
|
||||
'url': 'URL',
|
||||
'name': '%{Name}',
|
||||
'vendor': '%{Vendor}',
|
||||
'packager': '%{Packager}',
|
||||
'url': '%{URL}',
|
||||
'maintainer': None, # RPM doesn't have a maintainer field
|
||||
# -- the full version identity as one column
|
||||
'version': '%{VERSION}-%{RELEASE}',
|
||||
}
|
||||
)
|
||||
return _meta_map
|
||||
|
|
@ -46,17 +48,15 @@ async def run_rpm( # export
|
|||
async def query_packages( # export
|
||||
names: Collection[str] = [],
|
||||
ec: ExecContext | None = None,
|
||||
throw: bool = True,
|
||||
) -> Iterable[Package]:
|
||||
fmt_str = (
|
||||
'|'.join([(f'%{{{tag}}}' if tag else '')
|
||||
for tag in meta_map().values()]) + r'\n'
|
||||
)
|
||||
fmt_str = '|'.join(tag or '' for tag in meta_map().values()) + r'\n'
|
||||
opts = ['-q', '--queryformat', fmt_str]
|
||||
if not names:
|
||||
opts.append('-a')
|
||||
specs = await run_rpm(
|
||||
[*opts, *names],
|
||||
throw = True,
|
||||
throw = throw,
|
||||
sudo = False,
|
||||
mode = InputMode.NonInteractive,
|
||||
ec = ec
|
||||
|
|
@ -80,6 +80,69 @@ async def list_files(
|
|||
)
|
||||
return stdout.splitlines()
|
||||
|
||||
async def _file_name_version(
|
||||
path: str,
|
||||
ec: ExecContext | None = None,
|
||||
) -> tuple[str, str]:
|
||||
"""Return the (name, version-release) pair of a local rpm file."""
|
||||
out = await run_rpm(
|
||||
['-qp', '--queryformat', '%{NAME}\t%{VERSION}-%{RELEASE}', path],
|
||||
throw = True,
|
||||
sudo = False,
|
||||
mode = InputMode.NonInteractive,
|
||||
ec = ec
|
||||
)
|
||||
name, version = out.split('\t', 1)
|
||||
return name, version
|
||||
|
||||
async def install_packages(
|
||||
paths: Iterable[str],
|
||||
ec: ExecContext | None = None,
|
||||
force: bool = False,
|
||||
only_update: bool = False,
|
||||
) -> None:
|
||||
"""Install local rpm files.
|
||||
|
||||
Files are classified against the installed state of their
|
||||
packages: files for packages that are not installed are
|
||||
installed fresh, files with another version installed are
|
||||
upgraded, and files whose version is already installed are
|
||||
reinstalled with -U --reinstall when force is set and
|
||||
skipped otherwise, because plain -U refuses a package that
|
||||
is already installed. The upgrades run before the
|
||||
reinstalls: --reinstall aborts the whole transaction when a
|
||||
file of the incoming package differs from the installed
|
||||
one, which is exactly what an upgrade changes.
|
||||
|
||||
With only_update, a file for a package that is not installed
|
||||
raises before anything is installed.
|
||||
"""
|
||||
paths = list(paths)
|
||||
if not paths:
|
||||
return
|
||||
update: list[str] = []
|
||||
reinstall: list[str] = []
|
||||
not_installed: list[str] = []
|
||||
for path in paths:
|
||||
name, version = await _file_name_version(path, ec = ec)
|
||||
pkgs = list(await query_packages([name], ec = ec, throw = False))
|
||||
if not pkgs:
|
||||
(not_installed if only_update else update).append(path)
|
||||
continue
|
||||
if version in {p.version for p in pkgs if p.version}:
|
||||
reinstall.append(path)
|
||||
continue
|
||||
update.append(path)
|
||||
if not_installed:
|
||||
raise Exception(
|
||||
'only_update: package not installed for ' +
|
||||
'\n'.join(f' {p}' for p in not_installed)
|
||||
)
|
||||
if update:
|
||||
await run_rpm(['-U', *update], sudo = True, ec = ec)
|
||||
if force and reinstall:
|
||||
await run_rpm(['-U', '--reinstall', *reinstall], sudo = True, ec = ec)
|
||||
|
||||
def _parse_verify_output(
|
||||
pkg: str,
|
||||
output: str,
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ options:
|
|||
--download-only Only download packages from the repos, don't install them,
|
||||
yet (default: False)
|
||||
============= Running: jw-pkg.py -t ../../../.. --log-level info packages install --help
|
||||
usage: jw-pkg.py packages install [-h] [--only-update] [-F] [names ...]
|
||||
usage: jw-pkg.py packages install [-h] [--only-update] [-x] [-F] [names ...]
|
||||
|
||||
Install the distribution's notion of available packages
|
||||
|
||||
|
|
@ -99,6 +99,8 @@ options:
|
|||
-h, --help show this help message and exit
|
||||
--only-update Only update the listed packages, don't install them
|
||||
(default: False)
|
||||
-x, --exact Install local files directly, without resolving
|
||||
dependencies (default: False)
|
||||
-F, --fixed-strings Don't expand macros in <names> (default: False)
|
||||
============= Running: jw-pkg.py -t ../../../.. --log-level info packages ls --help
|
||||
usage: jw-pkg.py packages ls [-h] [names ...]
|
||||
|
|
|
|||
|
|
@ -256,7 +256,7 @@ class CannedRpmLocal(Local):
|
|||
elif '-ql' in cmd:
|
||||
out = '/etc/a/a.conf.jw-tmpl\n/etc/b/b.conf.jw-tmpl\n'
|
||||
else:
|
||||
out = 'a|v|p|https://jw.dev/a|m\nb|v|p|https://jw.dev/b|m\n'
|
||||
out = 'a|v|p|https://jw.dev/a|m|1.0.0-1\nb|v|p|https://jw.dev/b|m|1.0.0-1\n'
|
||||
return Result(stdout = out.encode(), status = 0)
|
||||
|
||||
cl = CannedRpmLocal()
|
||||
|
|
@ -286,4 +286,23 @@ sde = SuseDistro(
|
|||
assert asyncio.run(sde.pkg_files()) == []
|
||||
assert '--queryformat' in cl.cmds[-1]
|
||||
|
||||
# -- _install_local_files --
|
||||
|
||||
# -- With dependency resolution, the files go to the package manager;
|
||||
# DummyDistro._install() is a no-op
|
||||
asyncio.run(
|
||||
d._install_local_files(['/a/jw-base-run-1.0.0-42.rpm'], False, install_deps = True)
|
||||
)
|
||||
|
||||
# -- Without, the base implementation raises: there is no direct route
|
||||
try:
|
||||
asyncio.run(
|
||||
d._install_local_files(
|
||||
['/a/jw-base-run-1.0.0-42.rpm'], False, install_deps = False
|
||||
)
|
||||
)
|
||||
assert False, 'Should have raised'
|
||||
except NotImplementedError:
|
||||
pass
|
||||
|
||||
print('All Distro tests passed')
|
||||
|
|
|
|||
|
|
@ -3,29 +3,32 @@ from jw.pkg.lib.Package import Package, meta_tags
|
|||
# -- parse_spec_str --
|
||||
|
||||
p = Package.parse_spec_str(
|
||||
'jw-core|jw|Jan|https://example.com/jw-core|Jan <jan@example.com>'
|
||||
'jw-core|jw|Jan|https://example.com/jw-core|Jan <jan@example.com>|1.0.0-1'
|
||||
)
|
||||
assert p.name == 'jw-core'
|
||||
assert p.vendor == 'jw'
|
||||
assert p.packager == 'Jan'
|
||||
assert p.url == 'https://example.com/jw-core'
|
||||
assert p.maintainer == 'Jan <jan@example.com>'
|
||||
assert p.version == '1.0.0-1'
|
||||
|
||||
# An empty field is preserved as an empty string
|
||||
p = Package.parse_spec_str('name||||')
|
||||
p = Package.parse_spec_str('name|||||')
|
||||
assert p.name == 'name'
|
||||
assert p.vendor == ''
|
||||
assert p.packager == ''
|
||||
assert p.url == ''
|
||||
assert p.maintainer == ''
|
||||
assert p.version == ''
|
||||
|
||||
# A custom delimiter works
|
||||
p = Package.parse_spec_str('a,b,c,d,e', delimiter = ',')
|
||||
p = Package.parse_spec_str('a,b,c,d,e,f', delimiter = ',')
|
||||
assert p.name == 'a'
|
||||
assert p.maintainer == 'e'
|
||||
assert p.version == 'f'
|
||||
|
||||
# Wrong field counts raise
|
||||
for spec in ('a', 'a|b', 'a|b|c|d', 'a|b|c|d|e|f'):
|
||||
for spec in ('a', 'a|b', 'a|b|c', 'a|b|c|d|e', 'a|b|c|d|e|f|g'):
|
||||
try:
|
||||
Package.parse_spec_str(spec)
|
||||
assert False, f'Should have raised for "{spec}"'
|
||||
|
|
@ -35,8 +38,8 @@ for spec in ('a', 'a|b', 'a|b|c|d', 'a|b|c|d|e|f'):
|
|||
# -- parse_specs_str --
|
||||
|
||||
specs = (
|
||||
'jw-core|jw|Jan|https://example.com/jw-core|Jan\n'
|
||||
'jw-base|jw|Jan|https://example.com/jw-base|Jan'
|
||||
'jw-core|jw|Jan|https://example.com/jw-core|Jan|1.0.0-1\n'
|
||||
'jw-base|jw|Jan|https://example.com/jw-base|Jan|2.0.0-1'
|
||||
)
|
||||
packages = Package.parse_specs_str(specs)
|
||||
assert [p.name for p in packages] == ['jw-core', 'jw-base']
|
||||
|
|
@ -64,17 +67,19 @@ assert ordered == {
|
|||
'packager': '',
|
||||
'url': 'https://example.com/jw-core',
|
||||
'maintainer': 'Jan',
|
||||
'version': '',
|
||||
}
|
||||
|
||||
# -- __repr__ --
|
||||
|
||||
p = Package.parse_spec_str('jw-core|jw|Jan|url|Jan')
|
||||
p = Package.parse_spec_str('jw-core|jw|Jan|url|Jan|1.0.0-1')
|
||||
assert repr(p) == (
|
||||
'name : jw-core\n'
|
||||
'vendor : jw\n'
|
||||
'packager : Jan\n'
|
||||
'url : url\n'
|
||||
'maintainer : Jan'
|
||||
'maintainer : Jan\n'
|
||||
'version : 1.0.0-1'
|
||||
)
|
||||
|
||||
print('All Package tests passed')
|
||||
|
|
|
|||
4
test/unit/python/jw/pkg/lib/distros/Makefile
Normal file
4
test/unit/python/jw/pkg/lib/distros/Makefile
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
TOPDIR = ../../../../../../../
|
||||
|
||||
include $(TOPDIR)/make/proj.mk
|
||||
include $(JWBDIR)/make/dirs.mk
|
||||
7
test/unit/python/jw/pkg/lib/distros/debian/Makefile
Normal file
7
test/unit/python/jw/pkg/lib/distros/debian/Makefile
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
TOPDIR = ../../../../../../../..
|
||||
|
||||
include $(TOPDIR)/make/proj.mk
|
||||
include $(JWBDIR)/make/py-run.mk
|
||||
|
||||
all:
|
||||
test: run
|
||||
127
test/unit/python/jw/pkg/lib/distros/debian/test.py
Normal file
127
test/unit/python/jw/pkg/lib/distros/debian/test.py
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
from typing import override
|
||||
|
||||
from jw.pkg.lib.distros.debian.Distro import Distro as DebianDistro
|
||||
from jw.pkg.lib.ec.Local import Local
|
||||
from jw.pkg.lib.Result import Result
|
||||
|
||||
class DispatchLocal(Local):
|
||||
"""A canned execution context that answers dpkg queries and
|
||||
the apt simulation"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
files: dict[str, str],
|
||||
installed: dict[str, str],
|
||||
simulate: str,
|
||||
) -> None:
|
||||
super().__init__('local')
|
||||
self.files = files
|
||||
self.installed = installed
|
||||
self.simulate = simulate
|
||||
self.cmds: list[list[str]] = []
|
||||
|
||||
@override
|
||||
async def _run(
|
||||
self,
|
||||
cmd: list[str],
|
||||
wd: str | None,
|
||||
verbose: bool,
|
||||
cmd_input: bytes | None,
|
||||
mod_env: dict[str, str] | None,
|
||||
interactive: bool,
|
||||
log_prefix: str,
|
||||
) -> Result:
|
||||
self.cmds.append(cmd)
|
||||
if '-I' in cmd:
|
||||
return Result(stdout = self.files.get(cmd[-1], '').encode(), status = 0)
|
||||
if cmd[0].endswith('dpkg-query'):
|
||||
name = cmd[-1]
|
||||
return Result(
|
||||
stdout = self.installed.get(name, '').encode(),
|
||||
status = 0 if name in self.installed else 1,
|
||||
)
|
||||
if '/usr/bin/apt-get' in cmd and '-s' in cmd:
|
||||
return Result(stdout = self.simulate.encode(), status = 0)
|
||||
return Result(stdout = b'', status = 0)
|
||||
|
||||
def dpkg_args(cmd: list[str]) -> list[str]:
|
||||
"""The dpkg command without the sudo prefix and its options"""
|
||||
return cmd[cmd.index('/usr/bin/dpkg'):]
|
||||
|
||||
# -- A mixed transaction runs two installs, the upgrades first: the
|
||||
# distro installs local files with force, which keeps the reinstall
|
||||
# semantics for already-installed versions
|
||||
cl = DispatchLocal(
|
||||
files = {
|
||||
'/a/jw-base-run-1.0.0-42.deb': 'Package: jw-base\nVersion: 1.0.0-42',
|
||||
'/a/jw-base-run-1.0.0-41.deb': 'Package: jw-base\nVersion: 1.0.0-41',
|
||||
},
|
||||
installed = {'jw-base': 'jw-base|||||1.0.0-42'},
|
||||
simulate = 'Inst jw-base [1.0.0-41]\n',
|
||||
)
|
||||
d = DebianDistro(cl, id = 'debian')
|
||||
asyncio.run(
|
||||
d._install_local_files(
|
||||
['/a/jw-base-run-1.0.0-42.deb', '/a/jw-base-run-1.0.0-41.deb'],
|
||||
False,
|
||||
install_deps = False,
|
||||
)
|
||||
)
|
||||
installs = [dpkg_args(cmd) for cmd in cl.cmds if '-i' in cmd]
|
||||
assert installs == [
|
||||
['/usr/bin/dpkg', '-i', '/a/jw-base-run-1.0.0-41.deb'],
|
||||
[
|
||||
'/usr/bin/dpkg',
|
||||
'-i',
|
||||
'--force-reinstall',
|
||||
'/a/jw-base-run-1.0.0-42.deb',
|
||||
],
|
||||
]
|
||||
|
||||
# -- only_update raises on files whose package is not installed
|
||||
cl = DispatchLocal(
|
||||
files = {
|
||||
'/a/jw-base-run-1.0.0-42.deb': 'Package: jw-base\nVersion: 1.0.0-42',
|
||||
'/a/jw-foo-run-1.0.0-42.deb': 'Package: jw-foo\nVersion: 1.0.0-42',
|
||||
},
|
||||
installed = {'jw-base': 'jw-base|||||1.0.0-42'},
|
||||
simulate = '',
|
||||
)
|
||||
d = DebianDistro(cl, id = 'debian')
|
||||
try:
|
||||
asyncio.run(
|
||||
d._install_local_files(
|
||||
['/a/jw-base-run-1.0.0-42.deb', '/a/jw-foo-run-1.0.0-42.deb'],
|
||||
True,
|
||||
install_deps = False,
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
assert '/a/jw-foo-run-1.0.0-42.deb' in str(e)
|
||||
else:
|
||||
raise AssertionError('expected an exception')
|
||||
assert not [cmd for cmd in cl.cmds if '-i' in cmd]
|
||||
|
||||
# -- deps routes local files through the package manager
|
||||
cl = DispatchLocal(
|
||||
files = {'/a/jw-base-run-1.0.0-42.deb': 'Package: jw-base\nVersion: 1.0.0-42'},
|
||||
installed = {'jw-base': 'jw-base|||||1.0.0-42'},
|
||||
simulate = '',
|
||||
)
|
||||
d = DebianDistro(cl, id = 'debian')
|
||||
asyncio.run(
|
||||
d._install_local_files(['/a/jw-base-run-1.0.0-42.deb'], False, install_deps = True)
|
||||
)
|
||||
pm_cmds = [cmd for cmd in cl.cmds if '/usr/bin/apt-get' in cmd]
|
||||
assert len(pm_cmds) == 1
|
||||
args = pm_cmds[0][pm_cmds[0].index('/usr/bin/apt-get'):]
|
||||
assert 'install' in args
|
||||
assert '--no-install-recommends' in args
|
||||
assert args[-1] == '/a/jw-base-run-1.0.0-42.deb'
|
||||
assert not [cmd for cmd in cl.cmds if '-i' in cmd]
|
||||
|
||||
print('All debian distro tests passed')
|
||||
7
test/unit/python/jw/pkg/lib/distros/redhat/Makefile
Normal file
7
test/unit/python/jw/pkg/lib/distros/redhat/Makefile
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
TOPDIR = ../../../../../../../..
|
||||
|
||||
include $(TOPDIR)/make/proj.mk
|
||||
include $(JWBDIR)/make/py-run.mk
|
||||
|
||||
all:
|
||||
test: run
|
||||
134
test/unit/python/jw/pkg/lib/distros/redhat/test.py
Normal file
134
test/unit/python/jw/pkg/lib/distros/redhat/test.py
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
from typing import override
|
||||
|
||||
from jw.pkg.lib.distros.redhat.Distro import Distro as RedhatDistro
|
||||
from jw.pkg.lib.ec.Local import Local
|
||||
from jw.pkg.lib.Result import Result
|
||||
|
||||
class DispatchLocal(Local):
|
||||
"""A canned execution context that answers rpm queries"""
|
||||
|
||||
def __init__(self, files: dict[str, str], installed: dict[str, str]) -> None:
|
||||
super().__init__('local')
|
||||
self.files = files
|
||||
self.installed = installed
|
||||
self.cmds: list[list[str]] = []
|
||||
|
||||
@override
|
||||
async def _run(
|
||||
self,
|
||||
cmd: list[str],
|
||||
wd: str | None,
|
||||
verbose: bool,
|
||||
cmd_input: bytes | None,
|
||||
mod_env: dict[str, str] | None,
|
||||
interactive: bool,
|
||||
log_prefix: str,
|
||||
) -> Result:
|
||||
self.cmds.append(cmd)
|
||||
if '-qp' in cmd:
|
||||
return Result(stdout = self.files.get(cmd[-1], '').encode(), status = 0)
|
||||
if '-q' in cmd:
|
||||
name = cmd[-1]
|
||||
return Result(
|
||||
stdout = self.installed.get(name, '').encode(),
|
||||
status = 0 if name in self.installed else 1,
|
||||
)
|
||||
return Result(stdout = b'', status = 0)
|
||||
|
||||
def rpm_args(cmd: list[str]) -> list[str]:
|
||||
"""The rpm command without the sudo prefix and its options"""
|
||||
return cmd[cmd.index('/usr/bin/rpm'):]
|
||||
|
||||
# -- A mixed transaction runs two installs, the upgrades first: the
|
||||
# distro installs local files with force, which keeps the reinstall
|
||||
# semantics for already-installed versions
|
||||
cl = DispatchLocal(
|
||||
files = {
|
||||
'/a/jw-base-run-1.0.0-42.x86_64.rpm': 'jw-base\t1.0.0-42',
|
||||
'/a/jw-base-run-1.0.0-41.x86_64.rpm': 'jw-base\t1.0.0-41',
|
||||
},
|
||||
installed = {'jw-base': 'jw-base|||||1.0.0-42\n'},
|
||||
)
|
||||
d = RedhatDistro(cl, id = 'redhat')
|
||||
asyncio.run(
|
||||
d._install_local_files(
|
||||
['/a/jw-base-run-1.0.0-42.x86_64.rpm', '/a/jw-base-run-1.0.0-41.x86_64.rpm'],
|
||||
False,
|
||||
install_deps = False,
|
||||
)
|
||||
)
|
||||
installs = [rpm_args(cmd) for cmd in cl.cmds if '-U' in cmd]
|
||||
assert installs == [
|
||||
['/usr/bin/rpm', '-U', '/a/jw-base-run-1.0.0-41.x86_64.rpm'],
|
||||
[
|
||||
'/usr/bin/rpm',
|
||||
'-U',
|
||||
'--reinstall',
|
||||
'/a/jw-base-run-1.0.0-42.x86_64.rpm',
|
||||
],
|
||||
]
|
||||
|
||||
# -- A single file whose version is already installed is reinstalled
|
||||
cl = DispatchLocal(
|
||||
files = {'/a/jw-base-run-1.0.0-42.x86_64.rpm': 'jw-base\t1.0.0-42'},
|
||||
installed = {'jw-base': 'jw-base|||||1.0.0-42\n'},
|
||||
)
|
||||
d = RedhatDistro(cl, id = 'redhat')
|
||||
asyncio.run(
|
||||
d._install_local_files(
|
||||
['/a/jw-base-run-1.0.0-42.x86_64.rpm'], False, install_deps = False
|
||||
)
|
||||
)
|
||||
assert rpm_args(cl.cmds[-1]) == [
|
||||
'/usr/bin/rpm',
|
||||
'-U',
|
||||
'--reinstall',
|
||||
'/a/jw-base-run-1.0.0-42.x86_64.rpm',
|
||||
]
|
||||
|
||||
# -- only_update raises on files whose package is not installed
|
||||
cl = DispatchLocal(
|
||||
files = {
|
||||
'/a/jw-base-run-1.0.0-42.x86_64.rpm': 'jw-base\t1.0.0-42',
|
||||
'/a/jw-foo-run-1.0.0-42.x86_64.rpm': 'jw-foo\t1.0.0-42',
|
||||
},
|
||||
installed = {'jw-base': 'jw-base|||||1.0.0-42\n'},
|
||||
)
|
||||
d = RedhatDistro(cl, id = 'redhat')
|
||||
try:
|
||||
asyncio.run(
|
||||
d._install_local_files(
|
||||
['/a/jw-base-run-1.0.0-42.x86_64.rpm', '/a/jw-foo-run-1.0.0-42.x86_64.rpm'],
|
||||
True,
|
||||
install_deps = False,
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
assert '/a/jw-foo-run-1.0.0-42.x86_64.rpm' in str(e)
|
||||
else:
|
||||
raise AssertionError('expected an exception')
|
||||
assert not [cmd for cmd in cl.cmds if '-U' in cmd]
|
||||
|
||||
# -- deps routes local files through the package manager
|
||||
cl = DispatchLocal(
|
||||
files = {'/a/jw-base-run-1.0.0-42.x86_64.rpm': 'jw-base\t1.0.0-42'},
|
||||
installed = {'jw-base': 'jw-base|||||1.0.0-42\n'},
|
||||
)
|
||||
d = RedhatDistro(cl, id = 'redhat')
|
||||
asyncio.run(
|
||||
d._install_local_files(
|
||||
['/a/jw-base-run-1.0.0-42.x86_64.rpm'], False, install_deps = True
|
||||
)
|
||||
)
|
||||
pm_cmds = [cmd for cmd in cl.cmds if '/usr/bin/yum' in cmd]
|
||||
assert len(pm_cmds) == 1
|
||||
args = pm_cmds[0][pm_cmds[0].index('/usr/bin/yum'):]
|
||||
assert 'install' in args
|
||||
assert args[-1] == '/a/jw-base-run-1.0.0-42.x86_64.rpm'
|
||||
assert not [cmd for cmd in cl.cmds if '-U' in cmd]
|
||||
|
||||
print('All redhat distro tests passed')
|
||||
7
test/unit/python/jw/pkg/lib/distros/suse/Makefile
Normal file
7
test/unit/python/jw/pkg/lib/distros/suse/Makefile
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
TOPDIR = ../../../../../../../..
|
||||
|
||||
include $(TOPDIR)/make/proj.mk
|
||||
include $(JWBDIR)/make/py-run.mk
|
||||
|
||||
all:
|
||||
test: run
|
||||
134
test/unit/python/jw/pkg/lib/distros/suse/test.py
Normal file
134
test/unit/python/jw/pkg/lib/distros/suse/test.py
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
from typing import override
|
||||
|
||||
from jw.pkg.lib.distros.suse.Distro import Distro as SuseDistro
|
||||
from jw.pkg.lib.ec.Local import Local
|
||||
from jw.pkg.lib.Result import Result
|
||||
|
||||
class DispatchLocal(Local):
|
||||
"""A canned execution context that answers rpm queries"""
|
||||
|
||||
def __init__(self, files: dict[str, str], installed: dict[str, str]) -> None:
|
||||
super().__init__('local')
|
||||
self.files = files
|
||||
self.installed = installed
|
||||
self.cmds: list[list[str]] = []
|
||||
|
||||
@override
|
||||
async def _run(
|
||||
self,
|
||||
cmd: list[str],
|
||||
wd: str | None,
|
||||
verbose: bool,
|
||||
cmd_input: bytes | None,
|
||||
mod_env: dict[str, str] | None,
|
||||
interactive: bool,
|
||||
log_prefix: str,
|
||||
) -> Result:
|
||||
self.cmds.append(cmd)
|
||||
if '-qp' in cmd:
|
||||
return Result(stdout = self.files.get(cmd[-1], '').encode(), status = 0)
|
||||
if '-q' in cmd:
|
||||
name = cmd[-1]
|
||||
return Result(
|
||||
stdout = self.installed.get(name, '').encode(),
|
||||
status = 0 if name in self.installed else 1,
|
||||
)
|
||||
return Result(stdout = b'', status = 0)
|
||||
|
||||
def rpm_args(cmd: list[str]) -> list[str]:
|
||||
"""The rpm command without the sudo prefix and its options"""
|
||||
return cmd[cmd.index('/usr/bin/rpm'):]
|
||||
|
||||
# -- A mixed transaction runs two installs, the upgrades first: the
|
||||
# distro installs local files with force, which keeps the reinstall
|
||||
# semantics for already-installed versions
|
||||
cl = DispatchLocal(
|
||||
files = {
|
||||
'/a/jw-base-run-1.0.0-42.x86_64.rpm': 'jw-base\t1.0.0-42',
|
||||
'/a/jw-base-run-1.0.0-41.x86_64.rpm': 'jw-base\t1.0.0-41',
|
||||
},
|
||||
installed = {'jw-base': 'jw-base|||||1.0.0-42\n'},
|
||||
)
|
||||
d = SuseDistro(cl, id = 'suse')
|
||||
asyncio.run(
|
||||
d._install_local_files(
|
||||
['/a/jw-base-run-1.0.0-42.x86_64.rpm', '/a/jw-base-run-1.0.0-41.x86_64.rpm'],
|
||||
False,
|
||||
install_deps = False,
|
||||
)
|
||||
)
|
||||
installs = [rpm_args(cmd) for cmd in cl.cmds if '-U' in cmd]
|
||||
assert installs == [
|
||||
['/usr/bin/rpm', '-U', '/a/jw-base-run-1.0.0-41.x86_64.rpm'],
|
||||
[
|
||||
'/usr/bin/rpm',
|
||||
'-U',
|
||||
'--reinstall',
|
||||
'/a/jw-base-run-1.0.0-42.x86_64.rpm',
|
||||
],
|
||||
]
|
||||
|
||||
# -- A single file whose version is already installed is reinstalled
|
||||
cl = DispatchLocal(
|
||||
files = {'/a/jw-base-run-1.0.0-42.x86_64.rpm': 'jw-base\t1.0.0-42'},
|
||||
installed = {'jw-base': 'jw-base|||||1.0.0-42\n'},
|
||||
)
|
||||
d = SuseDistro(cl, id = 'suse')
|
||||
asyncio.run(
|
||||
d._install_local_files(
|
||||
['/a/jw-base-run-1.0.0-42.x86_64.rpm'], False, install_deps = False
|
||||
)
|
||||
)
|
||||
assert rpm_args(cl.cmds[-1]) == [
|
||||
'/usr/bin/rpm',
|
||||
'-U',
|
||||
'--reinstall',
|
||||
'/a/jw-base-run-1.0.0-42.x86_64.rpm',
|
||||
]
|
||||
|
||||
# -- only_update raises on files whose package is not installed
|
||||
cl = DispatchLocal(
|
||||
files = {
|
||||
'/a/jw-base-run-1.0.0-42.x86_64.rpm': 'jw-base\t1.0.0-42',
|
||||
'/a/jw-foo-run-1.0.0-42.x86_64.rpm': 'jw-foo\t1.0.0-42',
|
||||
},
|
||||
installed = {'jw-base': 'jw-base|||||1.0.0-42\n'},
|
||||
)
|
||||
d = SuseDistro(cl, id = 'suse')
|
||||
try:
|
||||
asyncio.run(
|
||||
d._install_local_files(
|
||||
['/a/jw-base-run-1.0.0-42.x86_64.rpm', '/a/jw-foo-run-1.0.0-42.x86_64.rpm'],
|
||||
True,
|
||||
install_deps = False,
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
assert '/a/jw-foo-run-1.0.0-42.x86_64.rpm' in str(e)
|
||||
else:
|
||||
raise AssertionError('expected an exception')
|
||||
assert not [cmd for cmd in cl.cmds if '-U' in cmd]
|
||||
|
||||
# -- deps routes local files through the package manager
|
||||
cl = DispatchLocal(
|
||||
files = {'/a/jw-base-run-1.0.0-42.x86_64.rpm': 'jw-base\t1.0.0-42'},
|
||||
installed = {'jw-base': 'jw-base|||||1.0.0-42\n'},
|
||||
)
|
||||
d = SuseDistro(cl, id = 'suse')
|
||||
asyncio.run(
|
||||
d._install_local_files(
|
||||
['/a/jw-base-run-1.0.0-42.x86_64.rpm'], False, install_deps = True
|
||||
)
|
||||
)
|
||||
pm_cmds = [cmd for cmd in cl.cmds if '/usr/bin/zypper' in cmd]
|
||||
assert len(pm_cmds) == 1
|
||||
args = pm_cmds[0][pm_cmds[0].index('/usr/bin/zypper'):]
|
||||
assert 'install' in args
|
||||
assert args[-1] == '/a/jw-base-run-1.0.0-42.x86_64.rpm'
|
||||
assert not [cmd for cmd in cl.cmds if '-U' in cmd]
|
||||
|
||||
print('All suse distro tests passed')
|
||||
8
test/unit/python/jw/pkg/lib/file_types/Makefile
Normal file
8
test/unit/python/jw/pkg/lib/file_types/Makefile
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
TOPDIR = ../../../../../../..
|
||||
|
||||
include $(TOPDIR)/make/proj.mk
|
||||
include $(JWBDIR)/make/py-run.mk
|
||||
|
||||
all:
|
||||
|
||||
test: run
|
||||
46
test/unit/python/jw/pkg/lib/file_types/test.py
Normal file
46
test/unit/python/jw/pkg/lib/file_types/test.py
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import stat
|
||||
|
||||
from jw.pkg.lib.base import StatResult
|
||||
from jw.pkg.lib.file_types import (
|
||||
FT_COMPOUND,
|
||||
FT_DIR,
|
||||
FT_REG,
|
||||
FT_TOP,
|
||||
FileType,
|
||||
file_types,
|
||||
)
|
||||
|
||||
# -- The compound hangs under the regular files: it is a file whose
|
||||
# content is gettable, and only its position in the tree makes
|
||||
# it listable, too
|
||||
assert FT_COMPOUND.parent is FT_REG
|
||||
assert FT_COMPOUND.is_subtype(FT_REG)
|
||||
assert not FT_COMPOUND.is_subtype(FT_DIR)
|
||||
assert FT_DIR.is_subtype(FT_TOP)
|
||||
assert FT_REG.is_subtype(FT_TOP)
|
||||
|
||||
def __stat(ftype: FileType) -> StatResult:
|
||||
return StatResult(0o400, 'u', 'g', 0, 0.0, 0.0, 0.0, ftype = ftype)
|
||||
|
||||
# -- The predicates answer from the type: a directory lists, a file
|
||||
# is gettable, a compound is both
|
||||
assert __stat(FT_DIR).is_dir and not __stat(FT_DIR).is_gettable
|
||||
assert not __stat(FT_REG).is_dir and __stat(FT_REG).is_gettable
|
||||
st = __stat(FT_COMPOUND)
|
||||
assert st.is_dir and st.is_gettable
|
||||
|
||||
# -- A mode-resolved stat answers the same way: the host's file
|
||||
# types are the tree's file types
|
||||
st = StatResult(stat.S_IFREG | 0o400, 'u', 'g', 0, 0.0, 0.0, 0.0)
|
||||
assert st.ftype is FT_REG and st.is_gettable and not st.is_dir
|
||||
st = StatResult(stat.S_IFDIR | 0o755, 'u', 'g', 0, 0.0, 0.0, 0.0)
|
||||
assert st.ftype is FT_DIR and st.is_dir and not st.is_gettable
|
||||
|
||||
# -- A mode the tree cannot resolve is an error
|
||||
try:
|
||||
file_types.from_mode(stat.S_IFLNK | 0o777)
|
||||
raise AssertionError('expected NotImplementedError')
|
||||
except NotImplementedError:
|
||||
pass
|
||||
|
||||
print('All file_types tests passed')
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue