Merge ppa-dev-tools:add_suite_module into ppa-dev-tools:main
- Git
- lp:ppa-dev-tools
- add_suite_module
- Merge into main
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) |
Related bugs: |
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 |
Commit message
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_
For static testing, I've been using the check-scripts tool from ubuntu-helpers:
$ check-scripts ./tests/
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
1 acct:amd64
2 acl:amd64
[...]
6042 zstd:amd64
6043 zsys:amd64
6044 zvmcloudconnect
Bryce Harrington (bryce) wrote : | # |
Thanks, I've extracted the import from the lintian commit and put it with the Suite class commit.
Bryce Harrington (bryce) wrote : | # |
Pushed
To git+ssh:
6220373..03aadb2 main -> main
Preview Diff
1 | diff --git a/.pylintrc b/.pylintrc |
2 | new file mode 100644 |
3 | index 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 |
24 | diff --git a/ppa/repository.py b/ppa/repository.py |
25 | index 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) |
99 | diff --git a/ppa/suite.py b/ppa/suite.py |
100 | new file mode 100644 |
101 | index 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 |
324 | diff --git a/tests/test_repository.py b/tests/test_repository.py |
325 | index 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' |
383 | diff --git a/tests/test_suite.py b/tests/test_suite.py |
384 | new file mode 100644 |
385 | index 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()) |
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.