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
diff --git a/ppa/suite.py b/ppa/suite.py
index 72eb8a6..4812f5d 100644
--- a/ppa/suite.py
+++ b/ppa/suite.py
@@ -43,6 +43,8 @@ class Suite:
4343
44 self._suite_name = suite_name44 self._suite_name = suite_name
45 self._cache_dir = cache_dir45 self._cache_dir = cache_dir
46 self._provides_table = None
47 self._rdepends_table = None
4648
47 def __repr__(self) -> str:49 def __repr__(self) -> str:
48 """Machine-parsable unique representation of object.50 """Machine-parsable unique representation of object.
@@ -64,6 +66,23 @@ class Suite:
64 """66 """
65 return f'{self._suite_name}'67 return f'{self._suite_name}'
6668
69 def _rebuild_lookup_tables(self) -> bool:
70 """Regenerates the provides and rdepends lookup tables.
71
72 :rtype: bool
73 :returns: True if tables were rebuilt, False otherwise"""
74 self._provides_table = {}
75 self._rdepends_table = {}
76 for source_name, source in self.sources.items():
77 print(source_name, source)
78 for build_dep_binary_name in source.build_dependencies.keys():
79 self._rdepends_table.setdefault(build_dep_binary_name, [])
80 self._rdepends_table[build_dep_binary_name].append(source)
81
82 for provided_binary_name in source.provides_binaries.keys():
83 self._provides_table[provided_binary_name] = source
84 return self._provides_table and self._rdepends_table
85
67 @property86 @property
68 @lru_cache87 @lru_cache
69 def info(self) -> dict[str, str]:88 def info(self) -> dict[str, str]:
@@ -184,6 +203,42 @@ class Suite:
184 raise ValueError(f'Could not load {binary_packages_dir}/Packages.xz')203 raise ValueError(f'Could not load {binary_packages_dir}/Packages.xz')
185 return binaries204 return binaries
186205
206 def dependent_packages(self, source_package: SourcePackage) -> dict[str, SourcePackage]:
207 """Relevant packages to run autotests against for a given source package.
208
209 Calculates the collection of build and reverse dependencies for
210 a given source package, that would be appropriate to re-run autopkgtests
211 on, using the given @param source_package's name as a trigger.
212
213 :param str source_package_name: The archive name of the source package.
214 :rtype: dict[str, SourcePackage]
215 :returns: Collection of source packages, keyed by name.
216 """
217 # Build the lookup table for provides and rdepends
218 if not self._provides_table or not self._rdepends_table:
219 if not self._rebuild_lookup_tables():
220 raise RuntimeError("Could not regenerate provides/rdepends lookup tables")
221
222 dependencies = {}
223
224 # Get source packages that depend on things we supply
225 for binary_package_name in source_package.provides_binaries.keys():
226 rdeps = self._rdepends_table.get(binary_package_name)
227 if rdeps:
228 for rdep_source in rdeps:
229 dependencies[rdep_source.name] = rdep_source
230
231 # Get source packages that provide our build dependencies
232 for build_dependency_name in source_package.build_dependencies.keys():
233 bdep_source = self._provides_table.get(build_dependency_name)
234 if not bdep_source:
235 raise RuntimeError(f'Could not get source object for bdep {build_dependency_name}')
236 dependencies[bdep_source.name] = bdep_source
237
238 if not dependencies:
239 raise RuntimeError(f'Could not calculate dependencies for {source_package.name}')
240 return dependencies
241
187242
188if __name__ == '__main__':243if __name__ == '__main__':
189 # pylint: disable=invalid-name244 # pylint: disable=invalid-name
diff --git a/tests/test_suite.py b/tests/test_suite.py
index 0203ecc..0dd430d 100644
--- a/tests/test_suite.py
+++ b/tests/test_suite.py
@@ -8,6 +8,8 @@
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# pylint: disable=protected-access,line-too-long
12
11"""Tests the Suite class as an interface to Apt suite records."""13"""Tests the Suite class as an interface to Apt suite records."""
1214
13import os15import os
@@ -367,3 +369,207 @@ def test_binaries(tmp_path, architectures, packages_contents, expected_binaries)
367 # Create the suite to wrapper our path and access Packages.xz369 # Create the suite to wrapper our path and access Packages.xz
368 suite = Suite(suite_dir.name, suite_dir)370 suite = Suite(suite_dir.name, suite_dir)
369 assert sorted(suite.binaries.keys()) == sorted(expected_binaries.keys())371 assert sorted(suite.binaries.keys()) == sorted(expected_binaries.keys())
372
373
374@pytest.mark.parametrize('sources, expected_rdepends, expected_provides', [
375 ({}, [], []),
376 (
377 {'a': SourcePackage({'Package': 'a', 'Version': 'x', 'Build-Depends': 'a', 'Binary': 'x'})},
378 ['a'],
379 ['x']
380 ),
381 (
382 {'a': SourcePackage({'Package': 'a', 'Version': 'x', 'Build-Depends': 'a, b, c', 'Binary': 'x'})},
383 ['a', 'b', 'c'],
384 ['x']
385 ),
386 (
387 {
388 'a': SourcePackage({'Package': 'a', 'Version': 'x', 'Binary': 'a1'}),
389 'b': SourcePackage({'Package': 'b', 'Version': 'x', 'Build-Depends': 'a1', 'Binary': 'b1'}),
390 'c': SourcePackage({'Package': 'c', 'Version': 'x', 'Build-Depends': 'a1, b1', 'Binary': 'c1'}),
391 },
392 ['a1', 'b1'],
393 ['a1', 'b1', 'c1']
394 ),
395 (
396 {'a': SourcePackage({'Package': 'a', 'Version': 'x', 'Binary': 'a'})},
397 [],
398 ['a']),
399 (
400 {'a': SourcePackage({'Package': 'a', 'Version': 'x', 'Binary': 'a, b, c'})},
401 [],
402 ['a', 'b', 'c']
403 ),
404 (
405 {
406 'dovecot': SourcePackage(
407 {
408 'Package': 'dovecot',
409 'Version': '1:2.3.19.1+dfsg1-2ubuntu4',
410 '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
411 'Architecture': 'any',
412 '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
413 'Testsuite-Triggers': 'lsb-release, python3, systemd-sysv',
414 }
415 )
416 },
417 [
418 'debhelper-compat',
419 'default-libmysqlclient-dev',
420 'krb5-multidev',
421 'libapparmor-dev',
422 'libbz2-dev',
423 'libcap-dev',
424 'libclucene-dev',
425 'libdb-dev',
426 'libexpat-dev',
427 'libexttextcat-dev',
428 'libicu-dev',
429 'libldap2-dev',
430 'liblua5.3-dev',
431 'liblz4-dev',
432 'liblzma-dev',
433 'libpam0g-dev',
434 'libpq-dev',
435 'libsasl2-dev',
436 'libsodium-dev',
437 'libsqlite3-dev',
438 'libssl-dev',
439 'libstemmer-dev',
440 'libsystemd-dev',
441 'libunwind-dev',
442 'libwrap0-dev',
443 'libzstd-dev',
444 'lsb-release',
445 'pkg-config',
446 'zlib1g-dev',
447 ],
448 [
449 'dovecot-core',
450 'dovecot-dev',
451 'dovecot-imapd',
452 'dovecot-pop3d',
453 'dovecot-lmtpd',
454 'dovecot-managesieved',
455 'dovecot-pgsql',
456 'dovecot-mysql',
457 'dovecot-sqlite',
458 'dovecot-ldap',
459 'dovecot-gssapi',
460 'dovecot-sieve',
461 'dovecot-solr',
462 'dovecot-lucene',
463 'dovecot-submissiond',
464 'dovecot-auth-lua',
465 ]
466 ),
467])
468def test_rebuild_tables(monkeypatch, sources, expected_rdepends, expected_provides):
469 """Checks generation of the internal lookup tables for provides and rdepends."""
470 monkeypatch.setattr(Suite, "sources", sources)
471 # Verify provides and rdepends table are as expected
472 suite = Suite('x', '/tmp')
473 suite._rebuild_lookup_tables()
474
475 assert sorted(suite._rdepends_table.keys()) == sorted(expected_rdepends)
476 assert sorted(suite._provides_table.keys()) == sorted(expected_provides)
477
478
479def test_rebuild_tables_mapping(monkeypatch):
480 """Checks the mapping of rdepends to provides in the generated tables.
481
482 The two lookup tables are essential to the rdepends test functionality
483 since they define the mappings between various source package provides
484 and depends. This test builds a synthetic collection of source packages,
485 generates the tables, and then verifies the tables can be used to lookup
486 the appropriate related packages.
487
488 For purposes of this test, we assume each source package provides
489 binaries of the same name appended with either '1' or '2'.
490
491 Also, note that the packages are set up with a circular dependency
492 (a depends on c, but c depends on a). This is an unhealthy
493 situation for an archive to be in, but it certainly does happen in
494 the wild. We're just setting it up that way for convenience since
495 we can then assume all provided binaries will be required by
496 something in the archive.
497 """
498 sources = {
499 'a': SourcePackage({'Package': 'a', 'Version': 'x', 'Build-Depends': 'c1', 'Binary': 'a1'}),
500 'b': SourcePackage({'Package': 'b', 'Version': 'x', 'Build-Depends': 'a1, c2', 'Binary': 'b1'}),
501 'c': SourcePackage({'Package': 'c', 'Version': 'x', 'Build-Depends': 'a1, b1', 'Binary': 'c1, c2'}),
502 }
503 monkeypatch.setattr(Suite, "sources", sources)
504 suite = Suite('x', '/tmp')
505 suite._rebuild_lookup_tables()
506
507 # Check the integrity of the lookup tables for the sources we gave it
508 for source in sources.values():
509 # Verify our dependency is satisfied by a SourcePackage in the collection
510 for dependency in source.build_dependencies:
511 assert dependency in suite._provides_table
512 package = suite._provides_table[dependency]
513 assert isinstance(package, SourcePackage)
514 assert dependency in [f"{package.name}1", f"{package.name}2"]
515
516 # Verify SourcePackages that depend on us can be located
517 for binary in source.provides_binaries:
518 assert binary in suite._rdepends_table
519 for package in suite._rdepends_table[binary]:
520 assert isinstance(package, SourcePackage)
521 assert binary in package.build_dependencies
522
523
524@pytest.mark.parametrize('sources_dict, source_package_name, expected_packages', [
525 pytest.param(
526 [{'Package': 'a', 'Version': 'x', 'Binary': 'a1'}], 'a', [],
527 marks=pytest.mark.xfail(reason='Does not handle undefined build-depends'),
528 ),
529 pytest.param(
530 [{'Package': 'a', 'Version': 'x', 'Build-Depends': None, 'Binary': 'a1'}], 'a', [],
531 marks=pytest.mark.xfail(reason='Does not handle undefined build-depends'),
532 ),
533 pytest.param(
534 [{'Package': 'a', 'Version': 'x', 'Build-Depends': '', 'Binary': 'a1'}], 'a', [],
535 marks=pytest.mark.xfail(reason='Does not handle undefined build-depends'),
536 ),
537
538 (
539 [
540 {'Package': 'a', 'Version': 'x', 'Build-Depends': 'b1', 'Binary': 'a1'},
541 {'Package': 'b', 'Version': 'x', 'Build-Depends': 'a1', 'Binary': 'b1'},
542 {'Package': 'c', 'Version': 'x', 'Build-Depends': 'b1', 'Binary': 'c1'},
543 ],
544 'a',
545 ['b'],
546 ),
547
548 (
549 [
550 {'Package': 'a', 'Version': 'x', 'Build-Depends': 'd1', 'Binary': 'a1'},
551 {'Package': 'b', 'Version': 'x', 'Build-Depends': 'a1', 'Binary': 'b1'},
552 {'Package': 'c', 'Version': 'x', 'Build-Depends': 'b1', 'Binary': 'c1, c2'},
553 {'Package': 'd', 'Version': 'x', 'Build-Depends': 'a1, b1, c2', 'Binary': 'd1'},
554 ],
555 'd',
556 ['a', 'b', 'c'],
557 ),
558])
559def test_dependent_packages(monkeypatch, sources_dict, source_package_name, expected_packages):
560 '''Checks that dependent_packages() returns the right packages to test.
561
562 This member function is the main API for looking up what packages
563 should have autopkgtests run, triggered against our desired package.
564 '''
565 sources = {pkg['Package']: SourcePackage(pkg) for pkg in sources_dict}
566
567 monkeypatch.setattr(
568 Suite,
569 "sources",
570 sources
571 )
572 suite = Suite('x', '/tmp')
573 source_package = sources[source_package_name]
574
575 assert sorted(suite.dependent_packages(source_package)) == expected_packages

Subscribers

People subscribed via source and target branches

to all changes: