Merge ppa-dev-tools:add_suite_module into ppa-dev-tools:main

Proposed by Bryce Harrington
Status: Merged
Merge reported by: Bryce Harrington
Merged at revision: e0d63243e9c09aaceaa519755361a4fe7b91aca8
Proposed branch: ppa-dev-tools:add_suite_module
Merge into: ppa-dev-tools:main
Diff against target: 719 lines (+586/-48)
5 files modified
.pylintrc (+16/-0)
ppa/repository.py (+9/-34)
ppa/suite.py (+219/-0)
tests/test_repository.py (+11/-14)
tests/test_suite.py (+331/-0)
Reviewer Review Type Date Requested Status
Athos Ribeiro (community) Needs Information
Sergio Durigan Junior Pending
Canonical Server packageset reviewers Pending
Canonical Server Reporter Pending
Review via email: mp+439032@code.launchpad.net

Description of the change

Splits out and fleshes out the Suite class from the Repository class. This will house the major logic for the rdepends testing functionality, but this initial branch just gets the basic structure in place for accessing the packages within an archive.

A Suite is defined as a particular pocket of a release, so that's like lunar-proposed or focal-backports. Internally it is further divided by component, IOW main, restricted, universe, multiverse. The Suite class encapsulates the logic for dealing with all these divisions.

As usual, the unit tests for Repository and Suite can be run via:

  $ pytest-3 tests/test_repository.py tests/test_suite.py

For static testing, I've been using the check-scripts tool from ubuntu-helpers:

  $ check-scripts ./tests/test_suite.py ./tests/test_repository.py ./ppa/repository.py ./ppa/suite.py

Last, there's also a smoketest for the Suite class, which can be used if you have a local mirror of the Apt repository (I made mine using the `apt-mirror` utility). Here's what the output looks like on my system:

  $ python3 -m ppa.suite
lunar-proposed
  series: lunar
  pocket: proposed
  components: main
  architectures: amd64, arm64, armhf, i386, ppc64el, riscv64, s390x
  sources: (203 items)
    0 adsys
    1 apt
    2 at-spi2-core
    [...]
    200 xorg-server
    201 xz-utils
    202 zip
  binaries: (931 items)
    0 adsys:amd64
    1 apt:amd64
    2 apt-doc:amd64
    [...]
    928 xz-utils:amd64
    929 zip:amd64
    930 zstd:amd64
lunar
  series: lunar
  pocket: release
  components: main
  architectures: amd64, arm64, armhf, i386, ppc64el, riscv64, s390x
  sources: (2369 items)
    0 aalib
    1 abseil
    2 accountsservice
    [...]
    2366 zsh
    2367 zsys
    2368 zvmcloudconnector
  binaries: (6045 items)
    0 accountsservice:amd64
    1 acct:amd64
    2 acl:amd64
    [...]
    6042 zstd:amd64
    6043 zsys:amd64
    6044 zvmcloudconnector-common:amd64

To post a comment you must log in.
Revision history for this message
Athos Ribeiro (athos-ribeiro) wrote :

Thanks, Bryce!

Overall, LGTM. I have a few comments inline below.

nitpick: The new Suit class is being imported in a commit created before the class was introduced.

review: Needs Information
Revision history for this message
Bryce Harrington (bryce) wrote :

Thanks, I've extracted the import from the lintian commit and put it with the Suite class commit.

Revision history for this message
Bryce Harrington (bryce) wrote :

Pushed
To git+ssh://git.launchpad.net/ppa-dev-tools
   6220373..03aadb2 main -> main

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/.pylintrc b/.pylintrc
2new file mode 100644
3index 0000000..75e9cfe
4--- /dev/null
5+++ b/.pylintrc
6@@ -0,0 +1,16 @@
7+[MASTER]
8+
9+# Disable the message, report, category or checker with the given id(s). You
10+# can either give multiple identifiers separated by comma (,) or put this
11+# option multiple times (only on the command line, not in the configuration
12+# file where it should appear only once). You can also use "--disable=all" to
13+# disable everything first and then reenable specific checks. For example, if
14+# you want to run only the similarities checker, you can use "--disable=all
15+# --enable=similarities". If you want to run only the classes checker, but have
16+# no Warning level messages displayed, use "--disable=all --enable=classes
17+# --disable=W".
18+disable=import-error,
19+ wrong-import-position
20+
21+[TYPECHECK]
22+generated-members=apt_pkg.*
23\ No newline at end of file
24diff --git a/ppa/repository.py b/ppa/repository.py
25index b763b3e..34c306e 100644
26--- a/ppa/repository.py
27+++ b/ppa/repository.py
28@@ -9,38 +9,12 @@
29 # Authors:
30 # Bryce Harrington <bryce@canonical.com>
31
32+"""Top-level code for analyzing an Ubuntu Apt repository."""
33+
34 import os.path
35 from functools import lru_cache
36-from distro_info import UbuntuDistroInfo
37-
38-
39-# TODO: Move Suite class to its own module
40-class Suite:
41- def __init__(self, cache_dir):
42- self.cache_dir = cache_dir
43-
44- def __repr__(self):
45- return "<Suite {}>".format(self.cache_dir)
46
47- @property
48- def series_codename(self) -> str:
49- return os.path.basename(self.cache_dir).split('-')[0]
50-
51- @property
52- def pocket(self) -> str:
53- suite = os.path.basename(self.cache_dir)
54- if '-' in suite:
55- return suite.split('-')[1]
56- else:
57- return 'release'
58-
59- @property
60- def components(self) -> list[str]:
61- return [
62- component
63- for component in os.listdir(self.cache_dir)
64- if os.path.isdir(os.path.join(self.cache_dir, component))
65- ]
66+from .suite import Suite
67
68
69 class Repository:
70@@ -78,10 +52,10 @@ class Repository:
71 :rtype: dict[str, Suite]
72 """
73 return {
74- release_pocket: Suite(os.path.join(self.cache_dir, release_pocket))
75- for release_pocket
76+ suite_name: Suite(suite_name, os.path.join(self.cache_dir, suite_name))
77+ for suite_name
78 in os.listdir(self.cache_dir)
79- if os.path.isdir(os.path.join(self.cache_dir, release_pocket))
80+ if os.path.isdir(os.path.join(self.cache_dir, suite_name))
81 }
82
83 def get_suite(self, series_codename: str, pocket: str) -> Suite:
84@@ -104,11 +78,12 @@ class Repository:
85
86
87 if __name__ == "__main__":
88+ import sys
89 from pprint import PrettyPrinter
90 pp = PrettyPrinter(indent=4)
91
92- local_repository_path = "/var/spool/apt-mirror/skel/archive.ubuntu.com/ubuntu"
93- local_dists_path = os.path.join(local_repository_path, "dists")
94+ LOCAL_REPOSITORY_PATH = "/var/spool/apt-mirror/skel/archive.ubuntu.com/ubuntu"
95+ local_dists_path = os.path.join(LOCAL_REPOSITORY_PATH, "dists")
96 if not os.path.exists(local_dists_path):
97 print("Error: Missing checkout")
98 sys.exit(1)
99diff --git a/ppa/suite.py b/ppa/suite.py
100new file mode 100644
101index 0000000..58fc8bc
102--- /dev/null
103+++ b/ppa/suite.py
104@@ -0,0 +1,219 @@
105+#!/usr/bin/env python3
106+# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
107+
108+# Copyright (C) 2022 Authors
109+#
110+# Released under GNU GPLv2 or later, read the file 'LICENSE.GPLv2+' for
111+# more information.
112+#
113+# Authors:
114+# Bryce Harrington <bryce@canonical.com>
115+
116+"""Interprets and analyzes an Ubuntu Apt suite (aka release-pocket)."""
117+
118+import os.path
119+from functools import lru_cache
120+
121+# pylint: disable = no-name-in-module
122+import apt_pkg
123+
124+
125+class Suite:
126+ """A pocket of a Ubuntu series collecting source and binary package releases.
127+
128+ Suites are named "<series>-<pocket>", such as "focal-updates" or
129+ "jammy-proposed". The same package can have different versions in
130+ each Suite, but within a Suite each package will have no more than
131+ one version available at a time.
132+ """
133+ def __init__(self, suite_name: str, cache_dir: str):
134+ """Initializes a new Suite object for a given release pocket.
135+
136+ :param str series_codename: The textual name of the Ubuntu release.
137+ :param str pocket: The pocket name ('release', 'proposed', 'backports', etc.)
138+ :param str cache_dir: The path to the given suite in the local Apt mirror.
139+ """
140+ if not suite_name:
141+ raise ValueError('undefined suite_name.')
142+ if not cache_dir:
143+ raise ValueError('undefined cache_dir.')
144+
145+ self._suite_name = suite_name
146+ self._cache_dir = cache_dir
147+
148+ def __repr__(self) -> str:
149+ """Machine-parsable unique representation of object.
150+
151+ :rtype: str
152+ :returns: Official string representation of the object.
153+ """
154+ return (
155+ f'{self.__class__.__name__}('
156+ f'suite_name={self._suite_name!r}, '
157+ f'cache_dir={self._cache_dir!r})'
158+ )
159+
160+ def __str__(self) -> str:
161+ """Human-readable textual description of the Suite.
162+
163+ :rtype: str
164+ :returns: Human-readable string.
165+ """
166+ return f'{self._suite_name}'
167+
168+ @property
169+ @lru_cache
170+ def info(self) -> dict[str, str]:
171+ """The parsed Apt Release file for the suite as a dict.
172+
173+ :rtype: dict[str, str]
174+ """
175+ with apt_pkg.TagFile(f'{self._cache_dir}/Release') as tagfile:
176+ info = next(tagfile)
177+
178+ if not info:
179+ raise ValueError('Could not load {self._cache_dir}/Release')
180+
181+ return info
182+
183+ @property
184+ def name(self) -> str:
185+ """The name of the suite as recorded in the apt database.
186+
187+ :rtype: str
188+ """
189+ return self.info.get('Suite', None)
190+
191+ @property
192+ def series_codename(self) -> str:
193+ """The textual name of the Ubuntu release for this suite.
194+
195+ :rtype: str
196+ """
197+ return self.name.split('-')[0]
198+
199+ @property
200+ def pocket(self) -> str:
201+ """The category of the archive (release, proposed, security, et al).
202+
203+ :rtype: str
204+ """
205+ if '-' not in self.name:
206+ return 'release'
207+ return self.name.split('-')[1]
208+
209+ @property
210+ def architectures(self) -> list[str]:
211+ """The list of CPU hardware types supported by this suite.
212+
213+ :rtype: list[str]
214+ """
215+ return self.info.get('Architectures', None).split()
216+
217+ @property
218+ def components(self) -> list[str]:
219+ """The sections of the archive (main, universe, etc.) provided
220+ in this suite.
221+
222+ :rtype: list[str]
223+ """
224+ return [
225+ component
226+ for component in os.listdir(self._cache_dir)
227+ if os.path.isdir(os.path.join(self._cache_dir, component))
228+ ]
229+
230+ @property
231+ @lru_cache
232+ def sources(self) -> dict[str, str]:
233+ """The collection of source packages included in this suite.
234+
235+ All source packages in all components are returned.
236+
237+ :rtype: dict[str, str]
238+ """
239+ sources = {}
240+ for comp in self.components:
241+ source_packages_dir = f'{self._cache_dir}/{comp}/source'
242+ with apt_pkg.TagFile(f'{source_packages_dir}/Sources.xz') as pkgs:
243+ for pkg in pkgs:
244+ name = pkg['Package']
245+ sources[name] = f'SourcePackage({name})'
246+
247+ if not sources:
248+ raise ValueError(f'Could not load {source_packages_dir}/Sources.xz')
249+
250+ return sources
251+
252+ @property
253+ @lru_cache
254+ def binaries(self) -> dict[str, str]:
255+ """The collection of binary Deb packages included in this suite.
256+
257+ All binary packages in all components are returned.
258+
259+ :rtype: dict[str, str]
260+ """
261+ binaries = {}
262+ for comp in self.components:
263+ for arch in self.architectures:
264+ binary_packages_dir = f'{self._cache_dir}/{comp}/binary-{arch}'
265+ try:
266+ with apt_pkg.TagFile(f'{binary_packages_dir}/Packages.xz') as pkgs:
267+ for pkg in pkgs:
268+ name = f'{pkg["Package"]}:{arch}'
269+ binaries[name] = f'BinaryPackage({pkg})'
270+ except apt_pkg.Error:
271+ # If an Apt repository is incomplete, such as if
272+ # a given architecture was not mirrored, still
273+ # note the binaries exist but mark their records
274+ # as missing.
275+ binaries[name] = None
276+
277+ if not binaries:
278+ raise ValueError(f'Could not load {binary_packages_dir}/Packages.xz')
279+
280+ return binaries
281+
282+
283+if __name__ == '__main__':
284+ # pylint: disable=invalid-name
285+ import sys
286+ from .repository import Repository
287+
288+ from pprint import PrettyPrinter
289+ pp = PrettyPrinter(indent=4)
290+
291+ LOCAL_REPOSITORY_PATH = '/var/spool/apt-mirror/skel/archive.ubuntu.com/ubuntu'
292+ local_dists_path = os.path.join(LOCAL_REPOSITORY_PATH, 'dists')
293+ if not os.path.exists(local_dists_path):
294+ print('Error: Missing checkout')
295+ sys.exit(1)
296+
297+ repository = Repository(cache_dir=local_dists_path)
298+ for suite in repository.suites.values():
299+ print(suite)
300+ print(f' series: {suite.series_codename}')
301+ print(f' pocket: {suite.pocket}')
302+ print(f' components: {", ".join(suite.components)}')
303+ print(f' architectures: {", ".join(suite.architectures)}')
304+
305+ num_sources = len(suite.sources)
306+ ellipses_shown = False
307+ print(f' sources: ({num_sources} items)')
308+ for i, source in enumerate(suite.sources):
309+ if i < 3 or i >= num_sources - 3:
310+ print(f' {i} {source}')
311+ elif not ellipses_shown:
312+ print(' [...]')
313+ ellipses_shown = True
314+
315+ num_binaries = len(suite.binaries)
316+ ellipses_shown = False
317+ print(f' binaries: ({num_binaries} items)')
318+ for i, binary in enumerate(suite.binaries):
319+ if i < 3 or i >= num_binaries - 3:
320+ print(f' {i} {binary}')
321+ elif not ellipses_shown:
322+ print(' [...]')
323+ ellipses_shown = True
324diff --git a/tests/test_repository.py b/tests/test_repository.py
325index 774b6bd..84cc734 100644
326--- a/tests/test_repository.py
327+++ b/tests/test_repository.py
328@@ -8,15 +8,16 @@
329 # Released under GNU GPLv2 or later, read the file 'LICENSE.GPLv2+' for
330 # more information.
331
332+"""Tests the Repository class as an interface to an Apt repository."""
333+
334 import os
335 import sys
336
337-import pytest
338-
339 sys.path.insert(0, os.path.realpath(
340 os.path.join(os.path.dirname(os.path.realpath(__file__)), "..")))
341
342-from ppa.repository import Repository, Suite
343+from ppa.repository import Repository
344+from ppa.suite import Suite
345
346
347 def test_object(tmp_path):
348@@ -29,27 +30,23 @@ def test_suites(tmp_path):
349 """Checks getting all suites from the repository."""
350 suites = ['a', 'b', 'b-0', 'b-1']
351 for suite in suites:
352- d = tmp_path / suite
353- d.mkdir()
354+ suite_dir = tmp_path / suite
355+ suite_dir.mkdir()
356
357 repository = Repository(tmp_path)
358 assert sorted(repository.suites.keys()) == sorted(suites)
359 for suite in repository.suites.values():
360- assert type(suite) is Suite
361+ assert isinstance(suite, Suite)
362
363
364 def test_get_suite(tmp_path):
365 """Checks getting a specific suite from the repository."""
366 suites = ['a', 'b', 'b-0', 'b-1']
367 for suite in suites:
368- d = tmp_path / suite
369- d.mkdir()
370- for component in ['x', 'y', 'z']:
371- c = d / component
372- c.mkdir()
373+ suite_dir = tmp_path / suite
374+ suite_dir.mkdir()
375
376 repository = Repository(tmp_path)
377 suite = repository.get_suite('b', '1')
378- assert suite.cache_dir == str(tmp_path / 'b-1')
379- assert suite.pocket == '1'
380- assert sorted(suite.components) == sorted(['x', 'y', 'z'])
381+ assert suite
382+ assert str(suite) == 'b-1'
383diff --git a/tests/test_suite.py b/tests/test_suite.py
384new file mode 100644
385index 0000000..7bfcaa1
386--- /dev/null
387+++ b/tests/test_suite.py
388@@ -0,0 +1,331 @@
389+#!/usr/bin/env python3
390+# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
391+
392+# Author: Bryce Harrington <bryce@canonical.com>
393+#
394+# Copyright (C) 2023 Bryce W. Harrington
395+#
396+# Released under GNU GPLv2 or later, read the file 'LICENSE.GPLv2+' for
397+# more information.
398+
399+"""Tests the Suite class as an interface to Apt suite records."""
400+
401+import os
402+import sys
403+
404+import lzma as xz
405+import pytest
406+
407+sys.path.insert(0, os.path.realpath(
408+ os.path.join(os.path.dirname(os.path.realpath(__file__)), '..')))
409+
410+from ppa.suite import Suite
411+
412+
413+@pytest.mark.parametrize('suite_name, cache_dir, expected_repr, expected_str', [
414+ ('x', 'x', "Suite(suite_name='x', cache_dir='x')", "x"),
415+ ('a-1', 'x', "Suite(suite_name='a-1', cache_dir='x')", "a-1"),
416+ ('b-2', '/tmp', "Suite(suite_name='b-2', cache_dir='/tmp')", "b-2"),
417+])
418+def test_object(suite_name, cache_dir, expected_repr, expected_str):
419+ """Checks that Suite objects can be instantiated."""
420+ suite = Suite(suite_name, cache_dir)
421+
422+ assert suite
423+ assert repr(suite) == expected_repr
424+ assert str(suite) == expected_str
425+
426+
427+@pytest.mark.parametrize('suite_name, cache_dir, expected_exception', [
428+ ('x', '', ValueError),
429+ ('', 'x', ValueError),
430+ ('', '', ValueError),
431+ ('a-1', None, ValueError),
432+ (None, 'x', ValueError),
433+ (None, None, ValueError),
434+])
435+def test_object_error(suite_name, cache_dir, expected_exception):
436+ """Checks that Suite objects handle invalid input properly."""
437+ with pytest.raises(expected_exception):
438+ suite = Suite(suite_name, cache_dir)
439+ assert suite
440+
441+
442+@pytest.mark.parametrize('release_contents, expected_info', [
443+ ('a: 1', {'a': '1'}),
444+ ("""
445+Origin: Ubuntu
446+Label: Ubuntu
447+Suite: lunar
448+Version: 23.04
449+Codename: lunar
450+Date: Tue, 28 Feb 2023 19:49:32 UTC
451+Architectures: amd64 arm64 armhf i386 ppc64el riscv64 s390x
452+Components: main restricted universe multiverse
453+Description: Ubuntu Lunar 23.04
454+ """, {
455+ 'Suite': 'lunar',
456+ 'Codename': 'lunar',
457+ 'Architectures': 'amd64 arm64 armhf i386 ppc64el riscv64 s390x',
458+ 'Components': 'main restricted universe multiverse',
459+ }), ("""
460+Origin: Ubuntu
461+Label: Ubuntu
462+Suite: lunar-proposed
463+Version: 23.04
464+Codename: lunar
465+Date: Tue, 28 Feb 2023 19:50:27 UTC
466+Architectures: amd64 arm64 armhf i386 ppc64el riscv64 s390x
467+Components: main restricted universe multiverse
468+Description: Ubuntu Lunar Proposed
469+NotAutomatic: yes
470+ButAutomaticUpgrades: yes
471+MD5Sum:
472+ 7de6b7c0ed6b4bfb662e07fbc449dfdd 148112816 Contents-amd64
473+ """, {
474+ 'Suite': 'lunar-proposed',
475+ 'Codename': 'lunar',
476+ 'NotAutomatic': 'yes'
477+ }),
478+])
479+def test_info(tmp_path, release_contents, expected_info):
480+ """Checks the parsing of info loaded from the Release file."""
481+ # Create Release file using release_contents in synthetic tree
482+ suite_dir = tmp_path / 'x'
483+ suite_dir.mkdir()
484+ release_file = suite_dir / 'Release'
485+ release_file.write_text(release_contents)
486+
487+ suite = Suite(suite_dir.name, suite_dir)
488+
489+ # Verify the expected items are present in the suite's info dict
490+ for key, value in expected_info.items():
491+ assert key in suite.info.keys()
492+ assert suite.info[key] == value
493+
494+
495+@pytest.mark.parametrize('info, expected_series_codename', [
496+ ({'Suite': 'x'}, 'x'),
497+ ({'Suite': 'x-y'}, 'x'),
498+ ({'Suite': 'lunar'}, 'lunar'),
499+ ({'Suite': 'lunar-proposed'}, 'lunar'),
500+ ({'Suite': 'focal-security'}, 'focal'),
501+])
502+def test_series_codename(monkeypatch, info, expected_series_codename):
503+ """Checks the codename is extracted properly from the suite name."""
504+ suite = Suite('x', '/tmp')
505+
506+ # Substitute in our fake test info in place of Suite's info() routine
507+ monkeypatch.setattr(Suite, "info", info)
508+
509+ assert suite.series_codename == expected_series_codename
510+
511+
512+@pytest.mark.parametrize('info, expected_pocket', [
513+ ({'Suite': 'x'}, 'release'),
514+ ({'Suite': 'x-y'}, 'y'),
515+ ({'Suite': 'lunar'}, 'release'),
516+ ({'Suite': 'lunar-proposed'}, 'proposed'),
517+ ({'Suite': 'focal-security'}, 'security'),
518+])
519+def test_pocket(monkeypatch, info, expected_pocket):
520+ """Checks the pocket is extracted properly from the suite name."""
521+ suite = Suite('x', '/tmp')
522+
523+ # Substitute in our fake test info in place of Suite's info() routine
524+ monkeypatch.setattr(Suite, "info", info)
525+
526+ assert suite.pocket == expected_pocket
527+
528+
529+@pytest.mark.parametrize('info, expected_architectures', [
530+ ({'Architectures': 'x y z'}, ['x', 'y', 'z']),
531+ ({'Architectures': 'x y z'}, ['x', 'y', 'z']),
532+ ({'Architectures': 'amd64 arm64 armhf i386 ppc64el riscv64 s390x'},
533+ ['amd64', 'arm64', 'armhf', 'i386', 'ppc64el', 'riscv64', 's390x']),
534+])
535+def test_architectures(monkeypatch, info, expected_architectures):
536+ """Checks that the architecture list is parsed from the info dict."""
537+ suite = Suite('x', '/tmp')
538+
539+ # Substitute in our fake test info in place of Suite's info() routine
540+ monkeypatch.setattr(Suite, "info", info)
541+
542+ assert sorted(suite.architectures) == sorted(expected_architectures)
543+
544+
545+@pytest.mark.parametrize('suite_name, component_paths, expected_components', [
546+ ('a-1', ['a-1/x', 'a-1/y', 'a-1/z'], ['x', 'y', 'z']),
547+ ('a-1', ['a-1/x', 'b-1/y', 'c-1/z'], ['x']),
548+ ('a-1', ['a-1/x', 'a-1/x/y', 'c-1/z/x'], ['x']),
549+ ('x', ['x/main', 'x/restricted', 'x/universe', 'x/multiverse'],
550+ ['main', 'restricted', 'universe', 'multiverse']),
551+])
552+def test_components(tmp_path, suite_name, component_paths, expected_components):
553+ """Checks that the components are read from the Apt directory tree.
554+
555+ The repository could have multiple suites (b-1, c-1, ...)
556+ so we specify that we're just looking for the components in
557+ @param suite_name.
558+ """
559+ # Stub in suite's directory structure with component subdirs
560+ for component_path in component_paths:
561+ component_dir = tmp_path / component_path
562+ component_dir.mkdir(parents=True)
563+
564+ suite = Suite(suite_name, tmp_path / suite_name)
565+
566+ assert sorted(suite.components) == sorted(expected_components)
567+
568+
569+@pytest.mark.parametrize('sources_contents, expected_sources', [
570+ ('Package: a', {'a': 'SourcePackage(a)'}),
571+ ("""
572+Package: aalib
573+
574+Package: abseil
575+
576+Package: accountsservice
577+
578+Package: acct
579+ """, {
580+ 'aalib': 'SourcePackage(aalib)',
581+ 'abseil': 'SourcePackage(abseil)',
582+ 'accountsservice': 'SourcePackage(accountsservice)',
583+ 'acct': 'SourcePackage(acct)',
584+ }),
585+ ("""
586+Package: libsigc++-2.0
587+Format: 3.0 (quilt)
588+Binary: libsigc++-2.0-0v5, libsigc++-2.0-dev, libsigc++-2.0-doc
589+Architecture: any all
590+Version: 2.12.0-1
591+Priority: optional
592+Section: devel
593+Maintainer: Debian GNOME Maintainers <pkg-gnome-maintainers@lists.alioth.debian.org>
594+Uploaders: Jeremy Bicha <jbicha@ubuntu.com>, Michael Biebl <biebl@debian.org>
595+Standards-Version: 4.6.1
596+Build-Depends: debhelper-compat (= 13), dh-sequence-gnome, docbook-xml, docbook-xsl, doxygen, graphviz, libxml2-utils <!nocheck>, meson (>= 0.50.0), mm-common (>= 1.0.0), python3-distutils, xsltproc
597+Homepage: https://libsigcplusplus.github.io/libsigcplusplus/
598+Vcs-Browser: https://salsa.debian.org/gnome-team/libsigcplusplus
599+Vcs-Git: https://salsa.debian.org/gnome-team/libsigcplusplus.git
600+Directory: pool/main/libs/libsigc++-2.0
601+Package-List:
602+ libsigc++-2.0-0v5 deb libs optional arch=any
603+ libsigc++-2.0-dev deb libdevel optional arch=any
604+ libsigc++-2.0-doc deb doc optional arch=all
605+Files:
606+ 23feb2cc5036384f94a3882c760a7eb4 2336 libsigc++-2.0_2.12.0-1.dsc
607+ 8685af8355138b1c48a6cd032e395303 163724 libsigc++-2.0_2.12.0.orig.tar.xz
608+ d60ca8c15750319f52d3b7eaeb6d99e1 10800 libsigc++-2.0_2.12.0-1.debian.tar.xz
609+Checksums-Sha1:
610+ 81840b1d39dc48350de207566c86b9f1ea1e22d2 2336 libsigc++-2.0_2.12.0-1.dsc
611+ f66e696482c4ff87968823ed17b294c159712824 163724 libsigc++-2.0_2.12.0.orig.tar.xz
612+ a699c88f7c91157af4c5cdd0f4d0ddebeea9092e 10800 libsigc++-2.0_2.12.0-1.debian.tar.xz
613+ """, # noqa: E501
614+ {
615+ 'libsigc++-2.0': 'SourcePackage(libsigc++-2.0)',
616+ }),
617+])
618+def test_sources(tmp_path, sources_contents, expected_sources):
619+ """Checks that the source packages are read from the Apt record.
620+
621+ We don't care about the SourcePackage object itself (the value in
622+ @param expected_sources is just a placeholder), but need to ensure
623+ the Sources.xz file is read and the expected list of packages
624+ parsed out of it.
625+ """
626+ # Create Sources.xz file using sources_contents in synthetic tree
627+ suite_dir = tmp_path / 'x'
628+ suite_dir.mkdir()
629+ component_dir = suite_dir / 'main'
630+ component_dir.mkdir()
631+ arch_dir = component_dir / 'source'
632+ arch_dir.mkdir()
633+ sources_file = arch_dir / 'Sources.xz'
634+ sources_file.write_bytes(xz.compress(str.encode(sources_contents)))
635+
636+ # Create the suite to wrapper our path and access Sources.xz
637+ suite = Suite(suite_dir.name, suite_dir)
638+
639+ assert sorted(suite.sources) == sorted(expected_sources)
640+
641+
642+@pytest.mark.parametrize('architectures, packages_contents, expected_binaries', [
643+ (['x'], 'Package: a', {'a:x': 'BinaryPackage(a)'}),
644+ (['amd64'], """
645+Package: aalib
646+
647+Package: abseil
648+
649+Package: accountsservice
650+
651+Package: acct
652+ """, {
653+ 'aalib:amd64': 'BinaryPackage(aalib)',
654+ 'abseil:amd64': 'BinaryPackage(abseil)',
655+ 'accountsservice:amd64': 'BinaryPackage(accountsservice)',
656+ 'acct:amd64': 'BinaryPackage(acct)',
657+ }),
658+ (['amd64'], """
659+Package: accountsservice
660+Architecture: amd64
661+Version: 22.08.8-1ubuntu4
662+Priority: optional
663+Section: gnome
664+Origin: Ubuntu
665+Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>
666+Original-Maintainer: Debian freedesktop.org maintainers <pkg-freedesktop-maintainers@lists.alioth.debian.org>
667+Bugs: https://bugs.launchpad.net/ubuntu/+filebug
668+Installed-Size: 504
669+Depends: dbus (>= 1.9.18), libaccountsservice0 (= 22.08.8-1ubuntu4), libc6 (>= 2.34), libglib2.0-0 (>= 2.63.5), libpolkit-gobject-1-0 (>= 0.99)
670+Recommends: default-logind | logind
671+Suggests: gnome-control-center
672+Filename: pool/main/a/accountsservice/accountsservice_22.08.8-1ubuntu4_amd64.deb
673+Size: 68364
674+MD5sum: a10447714f0ce3c5f607b7b27c0f9299
675+SHA1: 252482d8935d16b7fc5ced0c88819eeb7cad6c65
676+SHA256: 4e341a8e288d8f8f9ace6cf90a1dcc3211f06751d9bec3a68a6c539b0c711282
677+SHA512: eb91e21b4dfe38e9768e8ca50f30f94298d86f07e78525ab17df12eb3071ea21cfbdc2ade3e48bd4e22790aa6ce3406bb10fbd17d8d668f84de1c8adeee249cb
678+Homepage: https://www.freedesktop.org/wiki/Software/AccountsService/
679+Description: query and manipulate user account information
680+Task: ubuntu-desktop-minimal, ubuntu-desktop, ubuntu-desktop-raspi, ubuntu-wsl, kubuntu-desktop, xubuntu-minimal, xubuntu-desktop, lubuntu-desktop, ubuntustudio-desktop-core, ubuntustudio-desktop, ubuntukylin-desktop, ubuntu-mate-core, ubuntu-mate-desktop, ubuntu-budgie-desktop, ubuntu-budgie-desktop-raspi, ubuntu-unity-desktop, edubuntu-desktop-minimal, edubuntu-desktop, edubuntu-desktop-raspi, edubuntu-wsl
681+Description-md5: 8aeed0a03c7cd494f0c4b8d977483d7e
682+ """, { # noqa: E501
683+ 'accountsservice:amd64': 'BinaryPackage(accountsservice)'
684+ }),
685+ (
686+ ['amd64', 'arm64', 'armhf', 'i386', 'ppc64el', 'riscv64', 's390x'],
687+ 'Package: libsigc++-2.0', {
688+ 'libsigc++-2.0:amd64': 'BinaryPackage(libsigc++-2.0)',
689+ 'libsigc++-2.0:arm64': 'BinaryPackage(libsigc++-2.0)',
690+ 'libsigc++-2.0:armhf': 'BinaryPackage(libsigc++-2.0)',
691+ 'libsigc++-2.0:i386': 'BinaryPackage(libsigc++-2.0)',
692+ 'libsigc++-2.0:ppc64el': 'BinaryPackage(libsigc++-2.0)',
693+ 'libsigc++-2.0:riscv64': 'BinaryPackage(libsigc++-2.0)',
694+ 'libsigc++-2.0:s390x': 'BinaryPackage(libsigc++-2.0)',
695+ }
696+ ),
697+])
698+def test_binaries(tmp_path, architectures, packages_contents, expected_binaries):
699+ """Checks that the binary packages are read from the Apt record.
700+
701+ We don't care about the BinaryPackage (the value) itself, just that
702+ the package name and arch are registered correctly, and that typical
703+ Packages.xz files are processed as intended.
704+ """
705+ suite_dir = tmp_path / 'x'
706+ suite_dir.mkdir()
707+ release_file = suite_dir / 'Release'
708+ release_file.write_text(f'Architectures: {" ".join(architectures)}')
709+ component_dir = suite_dir / 'main'
710+ component_dir.mkdir()
711+ for arch in architectures:
712+ arch_dir = component_dir / f'binary-{arch}'
713+ arch_dir.mkdir()
714+ packages_file = arch_dir / 'Packages.xz'
715+ packages_file.write_bytes(xz.compress(str.encode(packages_contents)))
716+
717+ # Create the suite to wrapper our path and access Packages.xz
718+ suite = Suite(suite_dir.name, suite_dir)
719+ assert sorted(suite.binaries.keys()) == sorted(expected_binaries.keys())

Subscribers

People subscribed via source and target branches

to all changes: