From 94f7838c0b77b4e8ee00a3c49cab41404605fd75 Mon Sep 17 00:00:00 2001 From: Jan Lindemann Date: Tue, 8 Sep 2026 22:33:49 +0200 Subject: [PATCH 1/4] lib.Package: Add unit tests parse_spec_str(), parse_specs_str(), order_tags(), and __repr__ are pure text processing without test coverage, shared by the dpkg and rpm package manager backends. Add unit tests for valid and invalid spec strings, multi-line input with and without a trailing newline, tag ordering with default values, and the repr layout. Signed-off-by: Jan Lindemann Assisted-by: unsloth/Qwen3.8-27B-GGUF:Q4_K_M with pi.dev v0.85.1 --- test/unit/python/jw/pkg/lib/Package/Makefile | 7 ++ test/unit/python/jw/pkg/lib/Package/test.py | 80 ++++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 test/unit/python/jw/pkg/lib/Package/Makefile create mode 100644 test/unit/python/jw/pkg/lib/Package/test.py diff --git a/test/unit/python/jw/pkg/lib/Package/Makefile b/test/unit/python/jw/pkg/lib/Package/Makefile new file mode 100644 index 00000000..de693650 --- /dev/null +++ b/test/unit/python/jw/pkg/lib/Package/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/Package/test.py b/test/unit/python/jw/pkg/lib/Package/test.py new file mode 100644 index 00000000..09269a91 --- /dev/null +++ b/test/unit/python/jw/pkg/lib/Package/test.py @@ -0,0 +1,80 @@ +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 ' +) +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 ' + +# An empty field is preserved as an empty string +p = Package.parse_spec_str('name||||') +assert p.name == 'name' +assert p.vendor == '' +assert p.packager == '' +assert p.url == '' +assert p.maintainer == '' + +# A custom delimiter works +p = Package.parse_spec_str('a,b,c,d,e', delimiter = ',') +assert p.name == 'a' +assert p.maintainer == 'e' + +# Wrong field counts raise +for spec in ('a', 'a|b', 'a|b|c|d', 'a|b|c|d|e|f'): + try: + Package.parse_spec_str(spec) + assert False, f'Should have raised for "{spec}"' + except ValueError: + pass + +# -- 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' +) +packages = Package.parse_specs_str(specs) +assert [p.name for p in packages] == ['jw-core', 'jw-base'] +# A trailing newline does not create a phantom package +assert len(packages) == 2 +# A trailing newline is not needed +packages = Package.parse_specs_str(specs + '\n') +assert [p.name for p in packages] == ['jw-core', 'jw-base'] + +# An empty string parses to no packages +assert Package.parse_specs_str('') == [] + +# -- order_tags -- + +mapping = { + 'maintainer': 'Jan', + 'name': 'jw-core', + 'url': 'https://example.com/jw-core', +} +ordered = Package.order_tags(mapping) +assert list(ordered) == meta_tags +assert ordered == { + 'name': 'jw-core', + 'vendor': '', + 'packager': '', + 'url': 'https://example.com/jw-core', + 'maintainer': 'Jan', +} + +# -- __repr__ -- + +p = Package.parse_spec_str('jw-core|jw|Jan|url|Jan') +assert repr(p) == ( + 'name : jw-core\n' + 'vendor : jw\n' + 'packager : Jan\n' + 'url : url\n' + 'maintainer : Jan' +) + +print('All Package tests passed') -- 2.55.0 From b8d4402fe446820b0945b5cad54f86d247668cb9 Mon Sep 17 00:00:00 2001 From: Jan Lindemann Date: Tue, 8 Sep 2026 22:33:50 +0200 Subject: [PATCH 2/4] lib.ProcFilter: Add unit tests ProcPipeline and the run() helper are pure logic without test coverage. Add unit tests for the identity filter, chained filter execution order, the append() layouts, and the chain handling of run(). Signed-off-by: Jan Lindemann Assisted-by: unsloth/Qwen3.8-27B-GGUF:Q4_K_M with pi.dev v0.85.1 --- .../python/jw/pkg/lib/ProcFilter/Makefile | 7 ++ .../unit/python/jw/pkg/lib/ProcFilter/test.py | 64 +++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 test/unit/python/jw/pkg/lib/ProcFilter/Makefile create mode 100644 test/unit/python/jw/pkg/lib/ProcFilter/test.py diff --git a/test/unit/python/jw/pkg/lib/ProcFilter/Makefile b/test/unit/python/jw/pkg/lib/ProcFilter/Makefile new file mode 100644 index 00000000..de693650 --- /dev/null +++ b/test/unit/python/jw/pkg/lib/ProcFilter/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/ProcFilter/test.py b/test/unit/python/jw/pkg/lib/ProcFilter/test.py new file mode 100644 index 00000000..250c9b8f --- /dev/null +++ b/test/unit/python/jw/pkg/lib/ProcFilter/test.py @@ -0,0 +1,64 @@ +import asyncio + +from typing import override + +from jw.pkg.lib.ProcFilter import ProcFilter, ProcFilterIdentity, ProcPipeline, run +from jw.pkg.lib.Result import Result + +class Upper(ProcFilter): + + @override + async def _run(self, data: bytes | None) -> Result: + return Result(data.upper() if data else None, None, 0) + +class Exclaim(ProcFilter): + + @override + async def _run(self, data: bytes | None) -> Result: + return Result(data + b'!' if data else None, None, 0) + +# -- ProcFilterIdentity -- + +assert asyncio.run(ProcFilterIdentity().run(b'abc')).stdout == b'abc' +# A None input comes back as a result with no output; stdout would +# report b'' for a status-0 result, so use stdout_or_none here +assert asyncio.run(ProcFilterIdentity().run(None)).stdout_or_none is None +assert asyncio.run(ProcFilterIdentity().run(b'abc')).status == 0 + +# -- ProcPipeline -- + +# A single filter +pl = ProcPipeline(Upper()) +assert asyncio.run(pl.run(b'abc')).stdout == b'ABC' + +# Filters are applied in order +pl = ProcPipeline([Upper(), Exclaim()]) +assert asyncio.run(pl.run(b'abc')).stdout == b'ABC!' + +# append() accepts filters and iterables of filters, nested +pl = ProcPipeline() +pl.append(Upper()) +pl.append([Exclaim()]) +assert asyncio.run(pl.run(b'abc')).stdout == b'ABC!' + +# Input may be bytes or a Result +assert asyncio.run(pl.run(Result(b'abc', None, 0))).stdout == b'ABC!' + +# An empty pipeline is the identity +pl = ProcPipeline() +assert asyncio.run(pl.run(b'abc')).stdout == b'abc' + +# -- run() -- + +# Without a chain, bytes are wrapped and Results pass through +ret = asyncio.run(run(b'abc')) +assert isinstance(ret, Result) and ret.stdout == b'abc' +r = Result(b'xyz', None, 0) +assert asyncio.run(run(r)) is r + +# A single filter and a list of filters are both accepted +assert asyncio.run(run(b'abc', Upper())).stdout == b'ABC' +assert asyncio.run(run(b'abc', [Upper(), Exclaim()])).stdout == b'ABC!' +assert asyncio.run(run(b'abc', ProcPipeline([Upper()]))).stdout == b'ABC' + +print('All ProcFilter tests passed') -- 2.55.0 From 5a17cd68198ef0e100038f49d75d70074b8f8294 Mon Sep 17 00:00:00 2001 From: Jan Lindemann Date: Tue, 8 Sep 2026 22:37:09 +0200 Subject: [PATCH 3/4] lib.PackageFilter: Add unit tests PackageFilterString is pure regex logic without test coverage. Add unit tests for the url=~ filter, packages without a url, whitespace around the operator, and the rejection of unsupported filter definitions. Signed-off-by: Jan Lindemann Assisted-by: unsloth/Qwen3.8-27B-GGUF:Q4_K_M with pi.dev v0.85.1 --- .../python/jw/pkg/lib/PackageFilter/Makefile | 7 ++++ .../python/jw/pkg/lib/PackageFilter/test.py | 34 +++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 test/unit/python/jw/pkg/lib/PackageFilter/Makefile create mode 100644 test/unit/python/jw/pkg/lib/PackageFilter/test.py diff --git a/test/unit/python/jw/pkg/lib/PackageFilter/Makefile b/test/unit/python/jw/pkg/lib/PackageFilter/Makefile new file mode 100644 index 00000000..de693650 --- /dev/null +++ b/test/unit/python/jw/pkg/lib/PackageFilter/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/PackageFilter/test.py b/test/unit/python/jw/pkg/lib/PackageFilter/test.py new file mode 100644 index 00000000..f7c985bd --- /dev/null +++ b/test/unit/python/jw/pkg/lib/PackageFilter/test.py @@ -0,0 +1,34 @@ +from jw.pkg.lib.Package import Package +from jw.pkg.lib.PackageFilter import PackageFilterString + +pkg = Package(name = 'jw-core', url = 'https://example.com/jw-core') +pkg_other = Package(name = 'jw-base', url = 'https://other.org/jw-base') +pkg_nourl = Package(name = 'no-url') + +# -- url=~ filters -- + +f = PackageFilterString('url=~example\\.com') +assert f.match(pkg) +assert not f.match(pkg_other) +# A package without a url never matches +assert not f.match(pkg_nourl) + +# Whitespace around the operator is tolerated +f = PackageFilterString(' url =~ example') +assert f.match(pkg) + +# An unanchored regex matches anywhere in the url +f = PackageFilterString('url=~jw-core$') +assert f.match(pkg) +assert not f.match(pkg_other) + +# -- Unsupported definitions raise -- + +for definition in ('', 'url=example.com', 'name=jw-core', 'url ~ example'): + try: + PackageFilterString(definition) + assert False, f'Should have raised for "{definition}"' + except Exception: + pass + +print('All PackageFilter tests passed') -- 2.55.0 From 58cb969aaab2e04ae680e45542da4a4b827d2f4e Mon Sep 17 00:00:00 2001 From: Jan Lindemann Date: Tue, 8 Sep 2026 22:37:10 +0200 Subject: [PATCH 4/4] lib.TarIo: Add unit tests TarIo._match() and _filter_tar_file() are pure logic without test coverage. Add unit tests for the exact path matching, the unfiltered round trip, filtered extraction with a matched list, and a filter without hits. Signed-off-by: Jan Lindemann Assisted-by: unsloth/Qwen3.8-27B-GGUF:Q4_K_M with pi.dev v0.85.1 --- test/unit/python/jw/pkg/lib/TarIo/Makefile | 7 +++ test/unit/python/jw/pkg/lib/TarIo/test.py | 53 ++++++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 test/unit/python/jw/pkg/lib/TarIo/Makefile create mode 100644 test/unit/python/jw/pkg/lib/TarIo/test.py diff --git a/test/unit/python/jw/pkg/lib/TarIo/Makefile b/test/unit/python/jw/pkg/lib/TarIo/Makefile new file mode 100644 index 00000000..de693650 --- /dev/null +++ b/test/unit/python/jw/pkg/lib/TarIo/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/TarIo/test.py b/test/unit/python/jw/pkg/lib/TarIo/test.py new file mode 100644 index 00000000..56db44df --- /dev/null +++ b/test/unit/python/jw/pkg/lib/TarIo/test.py @@ -0,0 +1,53 @@ +import io +import tarfile + +from jw.pkg.lib.TarIo import TarIoTarFile + +def make_tar(files: dict[str, bytes]) -> bytes: + buf = io.BytesIO() + with tarfile.open(fileobj = buf, mode = 'w') as tf: + for name, content in files.items(): + info = tarfile.TarInfo(name) + info.size = len(content) + tf.addfile(info, io.BytesIO(content)) + return buf.getvalue() + +def members(blob: bytes) -> dict[str, bytes | None]: + ret = {} + with tarfile.open(fileobj = io.BytesIO(blob)) as tf: + for info in tf.getmembers(): + f = tf.extractfile(info) + ret[info.name] = f.read() if f is not None else None + return ret + +# -- Bypass the constructor: _filter_tar_file() and _match() do not use +# the copy contexts, so a bare instance is sufficient +tar = object.__new__(TarIoTarFile) + +files = { + 'a.txt': b'AAA', + 'dir/b.txt': b'BBB', + 'c.txt': b'CCC', +} +blob = make_tar(files) + +# -- _match is exact matching +assert tar._match('a.txt', ['a.txt', 'c.txt']) +assert not tar._match('a.txt', ['x.txt']) +assert not tar._match('a.txt', []) + +# -- _filter_tar_file without a filter is a round trip +ret = tar._filter_tar_file(blob) +assert members(ret) == files + +# -- With a filter, only the exact paths survive +matched: list[str] = [] +ret = tar._filter_tar_file(blob, ['a.txt', 'c.txt'], matched = matched) +assert members(ret) == {'a.txt': b'AAA', 'c.txt': b'CCC'} +assert sorted(matched) == ['a.txt', 'c.txt'] + +# -- A filter without hits yields an empty tar +ret = tar._filter_tar_file(blob, ['nope.txt']) +assert members(ret) == {} + +print('All TarIo tests passed') -- 2.55.0