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
diff --git a/.pylintrc b/.pylintrc
0new file mode 1006440new file mode 100644
index 0000000..75e9cfe
--- /dev/null
+++ b/.pylintrc
@@ -0,0 +1,16 @@
1[MASTER]
2
3# Disable the message, report, category or checker with the given id(s). You
4# can either give multiple identifiers separated by comma (,) or put this
5# option multiple times (only on the command line, not in the configuration
6# file where it should appear only once). You can also use "--disable=all" to
7# disable everything first and then reenable specific checks. For example, if
8# you want to run only the similarities checker, you can use "--disable=all
9# --enable=similarities". If you want to run only the classes checker, but have
10# no Warning level messages displayed, use "--disable=all --enable=classes
11# --disable=W".
12disable=import-error,
13 wrong-import-position
14
15[TYPECHECK]
16generated-members=apt_pkg.*
0\ No newline at end of file17\ No newline at end of file
diff --git a/ppa/repository.py b/ppa/repository.py
index b763b3e..34c306e 100644
--- a/ppa/repository.py
+++ b/ppa/repository.py
@@ -9,38 +9,12 @@
9# Authors:9# Authors:
10# Bryce Harrington <bryce@canonical.com>10# Bryce Harrington <bryce@canonical.com>
1111
12"""Top-level code for analyzing an Ubuntu Apt repository."""
13
12import os.path14import os.path
13from functools import lru_cache15from functools import lru_cache
14from distro_info import UbuntuDistroInfo
15
16
17# TODO: Move Suite class to its own module
18class Suite:
19 def __init__(self, cache_dir):
20 self.cache_dir = cache_dir
21
22 def __repr__(self):
23 return "<Suite {}>".format(self.cache_dir)
2416
25 @property17from .suite import Suite
26 def series_codename(self) -> str:
27 return os.path.basename(self.cache_dir).split('-')[0]
28
29 @property
30 def pocket(self) -> str:
31 suite = os.path.basename(self.cache_dir)
32 if '-' in suite:
33 return suite.split('-')[1]
34 else:
35 return 'release'
36
37 @property
38 def components(self) -> list[str]:
39 return [
40 component
41 for component in os.listdir(self.cache_dir)
42 if os.path.isdir(os.path.join(self.cache_dir, component))
43 ]
4418
4519
46class Repository:20class Repository:
@@ -78,10 +52,10 @@ class Repository:
78 :rtype: dict[str, Suite]52 :rtype: dict[str, Suite]
79 """53 """
80 return {54 return {
81 release_pocket: Suite(os.path.join(self.cache_dir, release_pocket))55 suite_name: Suite(suite_name, os.path.join(self.cache_dir, suite_name))
82 for release_pocket56 for suite_name
83 in os.listdir(self.cache_dir)57 in os.listdir(self.cache_dir)
84 if os.path.isdir(os.path.join(self.cache_dir, release_pocket))58 if os.path.isdir(os.path.join(self.cache_dir, suite_name))
85 }59 }
8660
87 def get_suite(self, series_codename: str, pocket: str) -> Suite:61 def get_suite(self, series_codename: str, pocket: str) -> Suite:
@@ -104,11 +78,12 @@ class Repository:
10478
10579
106if __name__ == "__main__":80if __name__ == "__main__":
81 import sys
107 from pprint import PrettyPrinter82 from pprint import PrettyPrinter
108 pp = PrettyPrinter(indent=4)83 pp = PrettyPrinter(indent=4)
10984
110 local_repository_path = "/var/spool/apt-mirror/skel/archive.ubuntu.com/ubuntu"85 LOCAL_REPOSITORY_PATH = "/var/spool/apt-mirror/skel/archive.ubuntu.com/ubuntu"
111 local_dists_path = os.path.join(local_repository_path, "dists")86 local_dists_path = os.path.join(LOCAL_REPOSITORY_PATH, "dists")
112 if not os.path.exists(local_dists_path):87 if not os.path.exists(local_dists_path):
113 print("Error: Missing checkout")88 print("Error: Missing checkout")
114 sys.exit(1)89 sys.exit(1)
diff --git a/ppa/suite.py b/ppa/suite.py
115new file mode 10064490new file mode 100644
index 0000000..58fc8bc
--- /dev/null
+++ b/ppa/suite.py
@@ -0,0 +1,219 @@
1#!/usr/bin/env python3
2# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
3
4# Copyright (C) 2022 Authors
5#
6# Released under GNU GPLv2 or later, read the file 'LICENSE.GPLv2+' for
7# more information.
8#
9# Authors:
10# Bryce Harrington <bryce@canonical.com>
11
12"""Interprets and analyzes an Ubuntu Apt suite (aka release-pocket)."""
13
14import os.path
15from functools import lru_cache
16
17# pylint: disable = no-name-in-module
18import apt_pkg
19
20
21class Suite:
22 """A pocket of a Ubuntu series collecting source and binary package releases.
23
24 Suites are named "<series>-<pocket>", such as "focal-updates" or
25 "jammy-proposed". The same package can have different versions in
26 each Suite, but within a Suite each package will have no more than
27 one version available at a time.
28 """
29 def __init__(self, suite_name: str, cache_dir: str):
30 """Initializes a new Suite object for a given release pocket.
31
32 :param str series_codename: The textual name of the Ubuntu release.
33 :param str pocket: The pocket name ('release', 'proposed', 'backports', etc.)
34 :param str cache_dir: The path to the given suite in the local Apt mirror.
35 """
36 if not suite_name:
37 raise ValueError('undefined suite_name.')
38 if not cache_dir:
39 raise ValueError('undefined cache_dir.')
40
41 self._suite_name = suite_name
42 self._cache_dir = cache_dir
43
44 def __repr__(self) -> str:
45 """Machine-parsable unique representation of object.
46
47 :rtype: str
48 :returns: Official string representation of the object.
49 """
50 return (
51 f'{self.__class__.__name__}('
52 f'suite_name={self._suite_name!r}, '
53 f'cache_dir={self._cache_dir!r})'
54 )
55
56 def __str__(self) -> str:
57 """Human-readable textual description of the Suite.
58
59 :rtype: str
60 :returns: Human-readable string.
61 """
62 return f'{self._suite_name}'
63
64 @property
65 @lru_cache
66 def info(self) -> dict[str, str]:
67 """The parsed Apt Release file for the suite as a dict.
68
69 :rtype: dict[str, str]
70 """
71 with apt_pkg.TagFile(f'{self._cache_dir}/Release') as tagfile:
72 info = next(tagfile)
73
74 if not info:
75 raise ValueError('Could not load {self._cache_dir}/Release')
76
77 return info
78
79 @property
80 def name(self) -> str:
81 """The name of the suite as recorded in the apt database.
82
83 :rtype: str
84 """
85 return self.info.get('Suite', None)
86
87 @property
88 def series_codename(self) -> str:
89 """The textual name of the Ubuntu release for this suite.
90
91 :rtype: str
92 """
93 return self.name.split('-')[0]
94
95 @property
96 def pocket(self) -> str:
97 """The category of the archive (release, proposed, security, et al).
98
99 :rtype: str
100 """
101 if '-' not in self.name:
102 return 'release'
103 return self.name.split('-')[1]
104
105 @property
106 def architectures(self) -> list[str]:
107 """The list of CPU hardware types supported by this suite.
108
109 :rtype: list[str]
110 """
111 return self.info.get('Architectures', None).split()
112
113 @property
114 def components(self) -> list[str]:
115 """The sections of the archive (main, universe, etc.) provided
116 in this suite.
117
118 :rtype: list[str]
119 """
120 return [
121 component
122 for component in os.listdir(self._cache_dir)
123 if os.path.isdir(os.path.join(self._cache_dir, component))
124 ]
125
126 @property
127 @lru_cache
128 def sources(self) -> dict[str, str]:
129 """The collection of source packages included in this suite.
130
131 All source packages in all components are returned.
132
133 :rtype: dict[str, str]
134 """
135 sources = {}
136 for comp in self.components:
137 source_packages_dir = f'{self._cache_dir}/{comp}/source'
138 with apt_pkg.TagFile(f'{source_packages_dir}/Sources.xz') as pkgs:
139 for pkg in pkgs:
140 name = pkg['Package']
141 sources[name] = f'SourcePackage({name})'
142
143 if not sources:
144 raise ValueError(f'Could not load {source_packages_dir}/Sources.xz')
145
146 return sources
147
148 @property
149 @lru_cache
150 def binaries(self) -> dict[str, str]:
151 """The collection of binary Deb packages included in this suite.
152
153 All binary packages in all components are returned.
154
155 :rtype: dict[str, str]
156 """
157 binaries = {}
158 for comp in self.components:
159 for arch in self.architectures:
160 binary_packages_dir = f'{self._cache_dir}/{comp}/binary-{arch}'
161 try:
162 with apt_pkg.TagFile(f'{binary_packages_dir}/Packages.xz') as pkgs:
163 for pkg in pkgs:
164 name = f'{pkg["Package"]}:{arch}'
165 binaries[name] = f'BinaryPackage({pkg})'
166 except apt_pkg.Error:
167 # If an Apt repository is incomplete, such as if
168 # a given architecture was not mirrored, still
169 # note the binaries exist but mark their records
170 # as missing.
171 binaries[name] = None
172
173 if not binaries:
174 raise ValueError(f'Could not load {binary_packages_dir}/Packages.xz')
175
176 return binaries
177
178
179if __name__ == '__main__':
180 # pylint: disable=invalid-name
181 import sys
182 from .repository import Repository
183
184 from pprint import PrettyPrinter
185 pp = PrettyPrinter(indent=4)
186
187 LOCAL_REPOSITORY_PATH = '/var/spool/apt-mirror/skel/archive.ubuntu.com/ubuntu'
188 local_dists_path = os.path.join(LOCAL_REPOSITORY_PATH, 'dists')
189 if not os.path.exists(local_dists_path):
190 print('Error: Missing checkout')
191 sys.exit(1)
192
193 repository = Repository(cache_dir=local_dists_path)
194 for suite in repository.suites.values():
195 print(suite)
196 print(f' series: {suite.series_codename}')
197 print(f' pocket: {suite.pocket}')
198 print(f' components: {", ".join(suite.components)}')
199 print(f' architectures: {", ".join(suite.architectures)}')
200
201 num_sources = len(suite.sources)
202 ellipses_shown = False
203 print(f' sources: ({num_sources} items)')
204 for i, source in enumerate(suite.sources):
205 if i < 3 or i >= num_sources - 3:
206 print(f' {i} {source}')
207 elif not ellipses_shown:
208 print(' [...]')
209 ellipses_shown = True
210
211 num_binaries = len(suite.binaries)
212 ellipses_shown = False
213 print(f' binaries: ({num_binaries} items)')
214 for i, binary in enumerate(suite.binaries):
215 if i < 3 or i >= num_binaries - 3:
216 print(f' {i} {binary}')
217 elif not ellipses_shown:
218 print(' [...]')
219 ellipses_shown = True
diff --git a/tests/test_repository.py b/tests/test_repository.py
index 774b6bd..84cc734 100644
--- a/tests/test_repository.py
+++ b/tests/test_repository.py
@@ -8,15 +8,16 @@
8# Released under GNU GPLv2 or later, read the file 'LICENSE.GPLv2+' for8# Released under GNU GPLv2 or later, read the file 'LICENSE.GPLv2+' for
9# more information.9# more information.
1010
11"""Tests the Repository class as an interface to an Apt repository."""
12
11import os13import os
12import sys14import sys
1315
14import pytest
15
16sys.path.insert(0, os.path.realpath(16sys.path.insert(0, os.path.realpath(
17 os.path.join(os.path.dirname(os.path.realpath(__file__)), "..")))17 os.path.join(os.path.dirname(os.path.realpath(__file__)), "..")))
1818
19from ppa.repository import Repository, Suite19from ppa.repository import Repository
20from ppa.suite import Suite
2021
2122
22def test_object(tmp_path):23def test_object(tmp_path):
@@ -29,27 +30,23 @@ def test_suites(tmp_path):
29 """Checks getting all suites from the repository."""30 """Checks getting all suites from the repository."""
30 suites = ['a', 'b', 'b-0', 'b-1']31 suites = ['a', 'b', 'b-0', 'b-1']
31 for suite in suites:32 for suite in suites:
32 d = tmp_path / suite33 suite_dir = tmp_path / suite
33 d.mkdir()34 suite_dir.mkdir()
3435
35 repository = Repository(tmp_path)36 repository = Repository(tmp_path)
36 assert sorted(repository.suites.keys()) == sorted(suites)37 assert sorted(repository.suites.keys()) == sorted(suites)
37 for suite in repository.suites.values():38 for suite in repository.suites.values():
38 assert type(suite) is Suite39 assert isinstance(suite, Suite)
3940
4041
41def test_get_suite(tmp_path):42def test_get_suite(tmp_path):
42 """Checks getting a specific suite from the repository."""43 """Checks getting a specific suite from the repository."""
43 suites = ['a', 'b', 'b-0', 'b-1']44 suites = ['a', 'b', 'b-0', 'b-1']
44 for suite in suites:45 for suite in suites:
45 d = tmp_path / suite46 suite_dir = tmp_path / suite
46 d.mkdir()47 suite_dir.mkdir()
47 for component in ['x', 'y', 'z']:
48 c = d / component
49 c.mkdir()
5048
51 repository = Repository(tmp_path)49 repository = Repository(tmp_path)
52 suite = repository.get_suite('b', '1')50 suite = repository.get_suite('b', '1')
53 assert suite.cache_dir == str(tmp_path / 'b-1')51 assert suite
54 assert suite.pocket == '1'52 assert str(suite) == 'b-1'
55 assert sorted(suite.components) == sorted(['x', 'y', 'z'])
diff --git a/tests/test_suite.py b/tests/test_suite.py
56new file mode 10064453new file mode 100644
index 0000000..7bfcaa1
--- /dev/null
+++ b/tests/test_suite.py
@@ -0,0 +1,331 @@
1#!/usr/bin/env python3
2# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
3
4# Author: Bryce Harrington <bryce@canonical.com>
5#
6# Copyright (C) 2023 Bryce W. Harrington
7#
8# Released under GNU GPLv2 or later, read the file 'LICENSE.GPLv2+' for
9# more information.
10
11"""Tests the Suite class as an interface to Apt suite records."""
12
13import os
14import sys
15
16import lzma as xz
17import pytest
18
19sys.path.insert(0, os.path.realpath(
20 os.path.join(os.path.dirname(os.path.realpath(__file__)), '..')))
21
22from ppa.suite import Suite
23
24
25@pytest.mark.parametrize('suite_name, cache_dir, expected_repr, expected_str', [
26 ('x', 'x', "Suite(suite_name='x', cache_dir='x')", "x"),
27 ('a-1', 'x', "Suite(suite_name='a-1', cache_dir='x')", "a-1"),
28 ('b-2', '/tmp', "Suite(suite_name='b-2', cache_dir='/tmp')", "b-2"),
29])
30def test_object(suite_name, cache_dir, expected_repr, expected_str):
31 """Checks that Suite objects can be instantiated."""
32 suite = Suite(suite_name, cache_dir)
33
34 assert suite
35 assert repr(suite) == expected_repr
36 assert str(suite) == expected_str
37
38
39@pytest.mark.parametrize('suite_name, cache_dir, expected_exception', [
40 ('x', '', ValueError),
41 ('', 'x', ValueError),
42 ('', '', ValueError),
43 ('a-1', None, ValueError),
44 (None, 'x', ValueError),
45 (None, None, ValueError),
46])
47def test_object_error(suite_name, cache_dir, expected_exception):
48 """Checks that Suite objects handle invalid input properly."""
49 with pytest.raises(expected_exception):
50 suite = Suite(suite_name, cache_dir)
51 assert suite
52
53
54@pytest.mark.parametrize('release_contents, expected_info', [
55 ('a: 1', {'a': '1'}),
56 ("""
57Origin: Ubuntu
58Label: Ubuntu
59Suite: lunar
60Version: 23.04
61Codename: lunar
62Date: Tue, 28 Feb 2023 19:49:32 UTC
63Architectures: amd64 arm64 armhf i386 ppc64el riscv64 s390x
64Components: main restricted universe multiverse
65Description: Ubuntu Lunar 23.04
66 """, {
67 'Suite': 'lunar',
68 'Codename': 'lunar',
69 'Architectures': 'amd64 arm64 armhf i386 ppc64el riscv64 s390x',
70 'Components': 'main restricted universe multiverse',
71 }), ("""
72Origin: Ubuntu
73Label: Ubuntu
74Suite: lunar-proposed
75Version: 23.04
76Codename: lunar
77Date: Tue, 28 Feb 2023 19:50:27 UTC
78Architectures: amd64 arm64 armhf i386 ppc64el riscv64 s390x
79Components: main restricted universe multiverse
80Description: Ubuntu Lunar Proposed
81NotAutomatic: yes
82ButAutomaticUpgrades: yes
83MD5Sum:
84 7de6b7c0ed6b4bfb662e07fbc449dfdd 148112816 Contents-amd64
85 """, {
86 'Suite': 'lunar-proposed',
87 'Codename': 'lunar',
88 'NotAutomatic': 'yes'
89 }),
90])
91def test_info(tmp_path, release_contents, expected_info):
92 """Checks the parsing of info loaded from the Release file."""
93 # Create Release file using release_contents in synthetic tree
94 suite_dir = tmp_path / 'x'
95 suite_dir.mkdir()
96 release_file = suite_dir / 'Release'
97 release_file.write_text(release_contents)
98
99 suite = Suite(suite_dir.name, suite_dir)
100
101 # Verify the expected items are present in the suite's info dict
102 for key, value in expected_info.items():
103 assert key in suite.info.keys()
104 assert suite.info[key] == value
105
106
107@pytest.mark.parametrize('info, expected_series_codename', [
108 ({'Suite': 'x'}, 'x'),
109 ({'Suite': 'x-y'}, 'x'),
110 ({'Suite': 'lunar'}, 'lunar'),
111 ({'Suite': 'lunar-proposed'}, 'lunar'),
112 ({'Suite': 'focal-security'}, 'focal'),
113])
114def test_series_codename(monkeypatch, info, expected_series_codename):
115 """Checks the codename is extracted properly from the suite name."""
116 suite = Suite('x', '/tmp')
117
118 # Substitute in our fake test info in place of Suite's info() routine
119 monkeypatch.setattr(Suite, "info", info)
120
121 assert suite.series_codename == expected_series_codename
122
123
124@pytest.mark.parametrize('info, expected_pocket', [
125 ({'Suite': 'x'}, 'release'),
126 ({'Suite': 'x-y'}, 'y'),
127 ({'Suite': 'lunar'}, 'release'),
128 ({'Suite': 'lunar-proposed'}, 'proposed'),
129 ({'Suite': 'focal-security'}, 'security'),
130])
131def test_pocket(monkeypatch, info, expected_pocket):
132 """Checks the pocket is extracted properly from the suite name."""
133 suite = Suite('x', '/tmp')
134
135 # Substitute in our fake test info in place of Suite's info() routine
136 monkeypatch.setattr(Suite, "info", info)
137
138 assert suite.pocket == expected_pocket
139
140
141@pytest.mark.parametrize('info, expected_architectures', [
142 ({'Architectures': 'x y z'}, ['x', 'y', 'z']),
143 ({'Architectures': 'x y z'}, ['x', 'y', 'z']),
144 ({'Architectures': 'amd64 arm64 armhf i386 ppc64el riscv64 s390x'},
145 ['amd64', 'arm64', 'armhf', 'i386', 'ppc64el', 'riscv64', 's390x']),
146])
147def test_architectures(monkeypatch, info, expected_architectures):
148 """Checks that the architecture list is parsed from the info dict."""
149 suite = Suite('x', '/tmp')
150
151 # Substitute in our fake test info in place of Suite's info() routine
152 monkeypatch.setattr(Suite, "info", info)
153
154 assert sorted(suite.architectures) == sorted(expected_architectures)
155
156
157@pytest.mark.parametrize('suite_name, component_paths, expected_components', [
158 ('a-1', ['a-1/x', 'a-1/y', 'a-1/z'], ['x', 'y', 'z']),
159 ('a-1', ['a-1/x', 'b-1/y', 'c-1/z'], ['x']),
160 ('a-1', ['a-1/x', 'a-1/x/y', 'c-1/z/x'], ['x']),
161 ('x', ['x/main', 'x/restricted', 'x/universe', 'x/multiverse'],
162 ['main', 'restricted', 'universe', 'multiverse']),
163])
164def test_components(tmp_path, suite_name, component_paths, expected_components):
165 """Checks that the components are read from the Apt directory tree.
166
167 The repository could have multiple suites (b-1, c-1, ...)
168 so we specify that we're just looking for the components in
169 @param suite_name.
170 """
171 # Stub in suite's directory structure with component subdirs
172 for component_path in component_paths:
173 component_dir = tmp_path / component_path
174 component_dir.mkdir(parents=True)
175
176 suite = Suite(suite_name, tmp_path / suite_name)
177
178 assert sorted(suite.components) == sorted(expected_components)
179
180
181@pytest.mark.parametrize('sources_contents, expected_sources', [
182 ('Package: a', {'a': 'SourcePackage(a)'}),
183 ("""
184Package: aalib
185
186Package: abseil
187
188Package: accountsservice
189
190Package: acct
191 """, {
192 'aalib': 'SourcePackage(aalib)',
193 'abseil': 'SourcePackage(abseil)',
194 'accountsservice': 'SourcePackage(accountsservice)',
195 'acct': 'SourcePackage(acct)',
196 }),
197 ("""
198Package: libsigc++-2.0
199Format: 3.0 (quilt)
200Binary: libsigc++-2.0-0v5, libsigc++-2.0-dev, libsigc++-2.0-doc
201Architecture: any all
202Version: 2.12.0-1
203Priority: optional
204Section: devel
205Maintainer: Debian GNOME Maintainers <pkg-gnome-maintainers@lists.alioth.debian.org>
206Uploaders: Jeremy Bicha <jbicha@ubuntu.com>, Michael Biebl <biebl@debian.org>
207Standards-Version: 4.6.1
208Build-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
209Homepage: https://libsigcplusplus.github.io/libsigcplusplus/
210Vcs-Browser: https://salsa.debian.org/gnome-team/libsigcplusplus
211Vcs-Git: https://salsa.debian.org/gnome-team/libsigcplusplus.git
212Directory: pool/main/libs/libsigc++-2.0
213Package-List:
214 libsigc++-2.0-0v5 deb libs optional arch=any
215 libsigc++-2.0-dev deb libdevel optional arch=any
216 libsigc++-2.0-doc deb doc optional arch=all
217Files:
218 23feb2cc5036384f94a3882c760a7eb4 2336 libsigc++-2.0_2.12.0-1.dsc
219 8685af8355138b1c48a6cd032e395303 163724 libsigc++-2.0_2.12.0.orig.tar.xz
220 d60ca8c15750319f52d3b7eaeb6d99e1 10800 libsigc++-2.0_2.12.0-1.debian.tar.xz
221Checksums-Sha1:
222 81840b1d39dc48350de207566c86b9f1ea1e22d2 2336 libsigc++-2.0_2.12.0-1.dsc
223 f66e696482c4ff87968823ed17b294c159712824 163724 libsigc++-2.0_2.12.0.orig.tar.xz
224 a699c88f7c91157af4c5cdd0f4d0ddebeea9092e 10800 libsigc++-2.0_2.12.0-1.debian.tar.xz
225 """, # noqa: E501
226 {
227 'libsigc++-2.0': 'SourcePackage(libsigc++-2.0)',
228 }),
229])
230def test_sources(tmp_path, sources_contents, expected_sources):
231 """Checks that the source packages are read from the Apt record.
232
233 We don't care about the SourcePackage object itself (the value in
234 @param expected_sources is just a placeholder), but need to ensure
235 the Sources.xz file is read and the expected list of packages
236 parsed out of it.
237 """
238 # Create Sources.xz file using sources_contents in synthetic tree
239 suite_dir = tmp_path / 'x'
240 suite_dir.mkdir()
241 component_dir = suite_dir / 'main'
242 component_dir.mkdir()
243 arch_dir = component_dir / 'source'
244 arch_dir.mkdir()
245 sources_file = arch_dir / 'Sources.xz'
246 sources_file.write_bytes(xz.compress(str.encode(sources_contents)))
247
248 # Create the suite to wrapper our path and access Sources.xz
249 suite = Suite(suite_dir.name, suite_dir)
250
251 assert sorted(suite.sources) == sorted(expected_sources)
252
253
254@pytest.mark.parametrize('architectures, packages_contents, expected_binaries', [
255 (['x'], 'Package: a', {'a:x': 'BinaryPackage(a)'}),
256 (['amd64'], """
257Package: aalib
258
259Package: abseil
260
261Package: accountsservice
262
263Package: acct
264 """, {
265 'aalib:amd64': 'BinaryPackage(aalib)',
266 'abseil:amd64': 'BinaryPackage(abseil)',
267 'accountsservice:amd64': 'BinaryPackage(accountsservice)',
268 'acct:amd64': 'BinaryPackage(acct)',
269 }),
270 (['amd64'], """
271Package: accountsservice
272Architecture: amd64
273Version: 22.08.8-1ubuntu4
274Priority: optional
275Section: gnome
276Origin: Ubuntu
277Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>
278Original-Maintainer: Debian freedesktop.org maintainers <pkg-freedesktop-maintainers@lists.alioth.debian.org>
279Bugs: https://bugs.launchpad.net/ubuntu/+filebug
280Installed-Size: 504
281Depends: dbus (>= 1.9.18), libaccountsservice0 (= 22.08.8-1ubuntu4), libc6 (>= 2.34), libglib2.0-0 (>= 2.63.5), libpolkit-gobject-1-0 (>= 0.99)
282Recommends: default-logind | logind
283Suggests: gnome-control-center
284Filename: pool/main/a/accountsservice/accountsservice_22.08.8-1ubuntu4_amd64.deb
285Size: 68364
286MD5sum: a10447714f0ce3c5f607b7b27c0f9299
287SHA1: 252482d8935d16b7fc5ced0c88819eeb7cad6c65
288SHA256: 4e341a8e288d8f8f9ace6cf90a1dcc3211f06751d9bec3a68a6c539b0c711282
289SHA512: eb91e21b4dfe38e9768e8ca50f30f94298d86f07e78525ab17df12eb3071ea21cfbdc2ade3e48bd4e22790aa6ce3406bb10fbd17d8d668f84de1c8adeee249cb
290Homepage: https://www.freedesktop.org/wiki/Software/AccountsService/
291Description: query and manipulate user account information
292Task: 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
293Description-md5: 8aeed0a03c7cd494f0c4b8d977483d7e
294 """, { # noqa: E501
295 'accountsservice:amd64': 'BinaryPackage(accountsservice)'
296 }),
297 (
298 ['amd64', 'arm64', 'armhf', 'i386', 'ppc64el', 'riscv64', 's390x'],
299 'Package: libsigc++-2.0', {
300 'libsigc++-2.0:amd64': 'BinaryPackage(libsigc++-2.0)',
301 'libsigc++-2.0:arm64': 'BinaryPackage(libsigc++-2.0)',
302 'libsigc++-2.0:armhf': 'BinaryPackage(libsigc++-2.0)',
303 'libsigc++-2.0:i386': 'BinaryPackage(libsigc++-2.0)',
304 'libsigc++-2.0:ppc64el': 'BinaryPackage(libsigc++-2.0)',
305 'libsigc++-2.0:riscv64': 'BinaryPackage(libsigc++-2.0)',
306 'libsigc++-2.0:s390x': 'BinaryPackage(libsigc++-2.0)',
307 }
308 ),
309])
310def test_binaries(tmp_path, architectures, packages_contents, expected_binaries):
311 """Checks that the binary packages are read from the Apt record.
312
313 We don't care about the BinaryPackage (the value) itself, just that
314 the package name and arch are registered correctly, and that typical
315 Packages.xz files are processed as intended.
316 """
317 suite_dir = tmp_path / 'x'
318 suite_dir.mkdir()
319 release_file = suite_dir / 'Release'
320 release_file.write_text(f'Architectures: {" ".join(architectures)}')
321 component_dir = suite_dir / 'main'
322 component_dir.mkdir()
323 for arch in architectures:
324 arch_dir = component_dir / f'binary-{arch}'
325 arch_dir.mkdir()
326 packages_file = arch_dir / 'Packages.xz'
327 packages_file.write_bytes(xz.compress(str.encode(packages_contents)))
328
329 # Create the suite to wrapper our path and access Packages.xz
330 suite = Suite(suite_dir.name, suite_dir)
331 assert sorted(suite.binaries.keys()) == sorted(expected_binaries.keys())

Subscribers

People subscribed via source and target branches

to all changes: