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

Proposed by Bryce Harrington
Status: Merged
Merge reported by: Bryce Harrington
Merged at revision: cf1fbe6179a33e75d5b2173f41b5d670aa58c65a
Proposed branch: ppa-dev-tools:add_dependent_packages_function
Merge into: ppa-dev-tools:main
Diff against target: 301 lines (+261/-0)
2 files modified
ppa/suite.py (+55/-0)
tests/test_suite.py (+206/-0)
Reviewer Review Type Date Requested Status
Athos Ribeiro (community) Approve
Canonical Server Reporter Pending
Review via email: mp+440454@code.launchpad.net

Description of the change

This branch finally implements the core mechanics of this feature, the mapping of provided binary packages of some source packages to the reverse dependencies of others. The dependent_packages() API is introduced to provide a way to look up for a given source packages, the other packages that should be tested along with it.

I mulled over a few different ideas for the API name before setting on "dependent_packages()" but am still unsure that's the best name. Alternative ideas would be welcome, and I reserve the option to rename this later if inspiration strikes. :-)

This API will be used to generate all the various trigger URLs. I wanted to include a smoketest to showcase this, but this is already plenty to review.

This branch just introduces the core data structure and basic lookups. It doesn't handle recursive lookups and some corner cases like packages with no build dependencies, but those enhancements should be straightforward modifications, and will be tackled in a future MP.

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

LGTM. Thanks, Bryce!

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

Thanks, landed:

To git+ssh://git.launchpad.net/ppa-dev-tools
   100b629..272c487 main -> main

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/ppa/suite.py b/ppa/suite.py
2index 72eb8a6..4812f5d 100644
3--- a/ppa/suite.py
4+++ b/ppa/suite.py
5@@ -43,6 +43,8 @@ class Suite:
6
7 self._suite_name = suite_name
8 self._cache_dir = cache_dir
9+ self._provides_table = None
10+ self._rdepends_table = None
11
12 def __repr__(self) -> str:
13 """Machine-parsable unique representation of object.
14@@ -64,6 +66,23 @@ class Suite:
15 """
16 return f'{self._suite_name}'
17
18+ def _rebuild_lookup_tables(self) -> bool:
19+ """Regenerates the provides and rdepends lookup tables.
20+
21+ :rtype: bool
22+ :returns: True if tables were rebuilt, False otherwise"""
23+ self._provides_table = {}
24+ self._rdepends_table = {}
25+ for source_name, source in self.sources.items():
26+ print(source_name, source)
27+ for build_dep_binary_name in source.build_dependencies.keys():
28+ self._rdepends_table.setdefault(build_dep_binary_name, [])
29+ self._rdepends_table[build_dep_binary_name].append(source)
30+
31+ for provided_binary_name in source.provides_binaries.keys():
32+ self._provides_table[provided_binary_name] = source
33+ return self._provides_table and self._rdepends_table
34+
35 @property
36 @lru_cache
37 def info(self) -> dict[str, str]:
38@@ -184,6 +203,42 @@ class Suite:
39 raise ValueError(f'Could not load {binary_packages_dir}/Packages.xz')
40 return binaries
41
42+ def dependent_packages(self, source_package: SourcePackage) -> dict[str, SourcePackage]:
43+ """Relevant packages to run autotests against for a given source package.
44+
45+ Calculates the collection of build and reverse dependencies for
46+ a given source package, that would be appropriate to re-run autopkgtests
47+ on, using the given @param source_package's name as a trigger.
48+
49+ :param str source_package_name: The archive name of the source package.
50+ :rtype: dict[str, SourcePackage]
51+ :returns: Collection of source packages, keyed by name.
52+ """
53+ # Build the lookup table for provides and rdepends
54+ if not self._provides_table or not self._rdepends_table:
55+ if not self._rebuild_lookup_tables():
56+ raise RuntimeError("Could not regenerate provides/rdepends lookup tables")
57+
58+ dependencies = {}
59+
60+ # Get source packages that depend on things we supply
61+ for binary_package_name in source_package.provides_binaries.keys():
62+ rdeps = self._rdepends_table.get(binary_package_name)
63+ if rdeps:
64+ for rdep_source in rdeps:
65+ dependencies[rdep_source.name] = rdep_source
66+
67+ # Get source packages that provide our build dependencies
68+ for build_dependency_name in source_package.build_dependencies.keys():
69+ bdep_source = self._provides_table.get(build_dependency_name)
70+ if not bdep_source:
71+ raise RuntimeError(f'Could not get source object for bdep {build_dependency_name}')
72+ dependencies[bdep_source.name] = bdep_source
73+
74+ if not dependencies:
75+ raise RuntimeError(f'Could not calculate dependencies for {source_package.name}')
76+ return dependencies
77+
78
79 if __name__ == '__main__':
80 # pylint: disable=invalid-name
81diff --git a/tests/test_suite.py b/tests/test_suite.py
82index 0203ecc..0dd430d 100644
83--- a/tests/test_suite.py
84+++ b/tests/test_suite.py
85@@ -8,6 +8,8 @@
86 # Released under GNU GPLv2 or later, read the file 'LICENSE.GPLv2+' for
87 # more information.
88
89+# pylint: disable=protected-access,line-too-long
90+
91 """Tests the Suite class as an interface to Apt suite records."""
92
93 import os
94@@ -367,3 +369,207 @@ def test_binaries(tmp_path, architectures, packages_contents, expected_binaries)
95 # Create the suite to wrapper our path and access Packages.xz
96 suite = Suite(suite_dir.name, suite_dir)
97 assert sorted(suite.binaries.keys()) == sorted(expected_binaries.keys())
98+
99+
100+@pytest.mark.parametrize('sources, expected_rdepends, expected_provides', [
101+ ({}, [], []),
102+ (
103+ {'a': SourcePackage({'Package': 'a', 'Version': 'x', 'Build-Depends': 'a', 'Binary': 'x'})},
104+ ['a'],
105+ ['x']
106+ ),
107+ (
108+ {'a': SourcePackage({'Package': 'a', 'Version': 'x', 'Build-Depends': 'a, b, c', 'Binary': 'x'})},
109+ ['a', 'b', 'c'],
110+ ['x']
111+ ),
112+ (
113+ {
114+ 'a': SourcePackage({'Package': 'a', 'Version': 'x', 'Binary': 'a1'}),
115+ 'b': SourcePackage({'Package': 'b', 'Version': 'x', 'Build-Depends': 'a1', 'Binary': 'b1'}),
116+ 'c': SourcePackage({'Package': 'c', 'Version': 'x', 'Build-Depends': 'a1, b1', 'Binary': 'c1'}),
117+ },
118+ ['a1', 'b1'],
119+ ['a1', 'b1', 'c1']
120+ ),
121+ (
122+ {'a': SourcePackage({'Package': 'a', 'Version': 'x', 'Binary': 'a'})},
123+ [],
124+ ['a']),
125+ (
126+ {'a': SourcePackage({'Package': 'a', 'Version': 'x', 'Binary': 'a, b, c'})},
127+ [],
128+ ['a', 'b', 'c']
129+ ),
130+ (
131+ {
132+ 'dovecot': SourcePackage(
133+ {
134+ 'Package': 'dovecot',
135+ 'Version': '1:2.3.19.1+dfsg1-2ubuntu4',
136+ 'Binary': 'dovecot-core, dovecot-dev, dovecot-imapd, dovecot-pop3d, dovecot-lmtpd, dovecot-managesieved, dovecot-pgsql, dovecot-mysql, dovecot-sqlite, dovecot-ldap, dovecot-gssapi, dovecot-sieve, dovecot-solr, dovecot-lucene, dovecot-submissiond, dovecot-auth-lua', # noqa: E501
137+ 'Architecture': 'any',
138+ 'Build-Depends': 'debhelper-compat (= 13), default-libmysqlclient-dev, krb5-multidev, libapparmor-dev [linux-any], libbz2-dev, libcap-dev [linux-any], libclucene-dev, libdb-dev, libexpat-dev, libexttextcat-dev, libicu-dev, libldap2-dev, liblua5.3-dev, liblz4-dev, liblzma-dev, libpam0g-dev, libpq-dev, libsasl2-dev, libsodium-dev, libsqlite3-dev, libssl-dev, libstemmer-dev, libsystemd-dev [linux-any], libunwind-dev [amd64 arm64 armel armhf hppa i386 ia64 mips mips64 mips64el mipsel powerpc powerpcspe ppc64 ppc64el sh4], libwrap0-dev, libzstd-dev, lsb-release, pkg-config, zlib1g-dev', # noqa: E501
139+ 'Testsuite-Triggers': 'lsb-release, python3, systemd-sysv',
140+ }
141+ )
142+ },
143+ [
144+ 'debhelper-compat',
145+ 'default-libmysqlclient-dev',
146+ 'krb5-multidev',
147+ 'libapparmor-dev',
148+ 'libbz2-dev',
149+ 'libcap-dev',
150+ 'libclucene-dev',
151+ 'libdb-dev',
152+ 'libexpat-dev',
153+ 'libexttextcat-dev',
154+ 'libicu-dev',
155+ 'libldap2-dev',
156+ 'liblua5.3-dev',
157+ 'liblz4-dev',
158+ 'liblzma-dev',
159+ 'libpam0g-dev',
160+ 'libpq-dev',
161+ 'libsasl2-dev',
162+ 'libsodium-dev',
163+ 'libsqlite3-dev',
164+ 'libssl-dev',
165+ 'libstemmer-dev',
166+ 'libsystemd-dev',
167+ 'libunwind-dev',
168+ 'libwrap0-dev',
169+ 'libzstd-dev',
170+ 'lsb-release',
171+ 'pkg-config',
172+ 'zlib1g-dev',
173+ ],
174+ [
175+ 'dovecot-core',
176+ 'dovecot-dev',
177+ 'dovecot-imapd',
178+ 'dovecot-pop3d',
179+ 'dovecot-lmtpd',
180+ 'dovecot-managesieved',
181+ 'dovecot-pgsql',
182+ 'dovecot-mysql',
183+ 'dovecot-sqlite',
184+ 'dovecot-ldap',
185+ 'dovecot-gssapi',
186+ 'dovecot-sieve',
187+ 'dovecot-solr',
188+ 'dovecot-lucene',
189+ 'dovecot-submissiond',
190+ 'dovecot-auth-lua',
191+ ]
192+ ),
193+])
194+def test_rebuild_tables(monkeypatch, sources, expected_rdepends, expected_provides):
195+ """Checks generation of the internal lookup tables for provides and rdepends."""
196+ monkeypatch.setattr(Suite, "sources", sources)
197+ # Verify provides and rdepends table are as expected
198+ suite = Suite('x', '/tmp')
199+ suite._rebuild_lookup_tables()
200+
201+ assert sorted(suite._rdepends_table.keys()) == sorted(expected_rdepends)
202+ assert sorted(suite._provides_table.keys()) == sorted(expected_provides)
203+
204+
205+def test_rebuild_tables_mapping(monkeypatch):
206+ """Checks the mapping of rdepends to provides in the generated tables.
207+
208+ The two lookup tables are essential to the rdepends test functionality
209+ since they define the mappings between various source package provides
210+ and depends. This test builds a synthetic collection of source packages,
211+ generates the tables, and then verifies the tables can be used to lookup
212+ the appropriate related packages.
213+
214+ For purposes of this test, we assume each source package provides
215+ binaries of the same name appended with either '1' or '2'.
216+
217+ Also, note that the packages are set up with a circular dependency
218+ (a depends on c, but c depends on a). This is an unhealthy
219+ situation for an archive to be in, but it certainly does happen in
220+ the wild. We're just setting it up that way for convenience since
221+ we can then assume all provided binaries will be required by
222+ something in the archive.
223+ """
224+ sources = {
225+ 'a': SourcePackage({'Package': 'a', 'Version': 'x', 'Build-Depends': 'c1', 'Binary': 'a1'}),
226+ 'b': SourcePackage({'Package': 'b', 'Version': 'x', 'Build-Depends': 'a1, c2', 'Binary': 'b1'}),
227+ 'c': SourcePackage({'Package': 'c', 'Version': 'x', 'Build-Depends': 'a1, b1', 'Binary': 'c1, c2'}),
228+ }
229+ monkeypatch.setattr(Suite, "sources", sources)
230+ suite = Suite('x', '/tmp')
231+ suite._rebuild_lookup_tables()
232+
233+ # Check the integrity of the lookup tables for the sources we gave it
234+ for source in sources.values():
235+ # Verify our dependency is satisfied by a SourcePackage in the collection
236+ for dependency in source.build_dependencies:
237+ assert dependency in suite._provides_table
238+ package = suite._provides_table[dependency]
239+ assert isinstance(package, SourcePackage)
240+ assert dependency in [f"{package.name}1", f"{package.name}2"]
241+
242+ # Verify SourcePackages that depend on us can be located
243+ for binary in source.provides_binaries:
244+ assert binary in suite._rdepends_table
245+ for package in suite._rdepends_table[binary]:
246+ assert isinstance(package, SourcePackage)
247+ assert binary in package.build_dependencies
248+
249+
250+@pytest.mark.parametrize('sources_dict, source_package_name, expected_packages', [
251+ pytest.param(
252+ [{'Package': 'a', 'Version': 'x', 'Binary': 'a1'}], 'a', [],
253+ marks=pytest.mark.xfail(reason='Does not handle undefined build-depends'),
254+ ),
255+ pytest.param(
256+ [{'Package': 'a', 'Version': 'x', 'Build-Depends': None, 'Binary': 'a1'}], 'a', [],
257+ marks=pytest.mark.xfail(reason='Does not handle undefined build-depends'),
258+ ),
259+ pytest.param(
260+ [{'Package': 'a', 'Version': 'x', 'Build-Depends': '', 'Binary': 'a1'}], 'a', [],
261+ marks=pytest.mark.xfail(reason='Does not handle undefined build-depends'),
262+ ),
263+
264+ (
265+ [
266+ {'Package': 'a', 'Version': 'x', 'Build-Depends': 'b1', 'Binary': 'a1'},
267+ {'Package': 'b', 'Version': 'x', 'Build-Depends': 'a1', 'Binary': 'b1'},
268+ {'Package': 'c', 'Version': 'x', 'Build-Depends': 'b1', 'Binary': 'c1'},
269+ ],
270+ 'a',
271+ ['b'],
272+ ),
273+
274+ (
275+ [
276+ {'Package': 'a', 'Version': 'x', 'Build-Depends': 'd1', 'Binary': 'a1'},
277+ {'Package': 'b', 'Version': 'x', 'Build-Depends': 'a1', 'Binary': 'b1'},
278+ {'Package': 'c', 'Version': 'x', 'Build-Depends': 'b1', 'Binary': 'c1, c2'},
279+ {'Package': 'd', 'Version': 'x', 'Build-Depends': 'a1, b1, c2', 'Binary': 'd1'},
280+ ],
281+ 'd',
282+ ['a', 'b', 'c'],
283+ ),
284+])
285+def test_dependent_packages(monkeypatch, sources_dict, source_package_name, expected_packages):
286+ '''Checks that dependent_packages() returns the right packages to test.
287+
288+ This member function is the main API for looking up what packages
289+ should have autopkgtests run, triggered against our desired package.
290+ '''
291+ sources = {pkg['Package']: SourcePackage(pkg) for pkg in sources_dict}
292+
293+ monkeypatch.setattr(
294+ Suite,
295+ "sources",
296+ sources
297+ )
298+ suite = Suite('x', '/tmp')
299+ source_package = sources[source_package_name]
300+
301+ assert sorted(suite.dependent_packages(source_package)) == expected_packages

Subscribers

People subscribed via source and target branches

to all changes: