Merge ~racb/git-ubuntu:interleave-publication-dates into git-ubuntu:master

Proposed by Robie Basak
Status: Merged
Merged at revision: 7a70527cca95da0ef5f2d8aaf565e6441a663558
Proposed branch: ~racb/git-ubuntu:interleave-publication-dates
Merge into: git-ubuntu:master
Diff against target: 447 lines (+300/-51)
4 files modified
gitubuntu/git_repository.py (+7/-0)
gitubuntu/importer.py (+52/-44)
gitubuntu/source_information.py (+117/-7)
gitubuntu/source_information_test.py (+124/-0)
Reviewer Review Type Date Requested Status
Server Team CI bot continuous-integration Approve
Bryce Harrington Approve
Review via email: mp+382510@code.launchpad.net

Commit message

Make Jenkins happy

To post a comment you must log in.
Revision history for this message
Server Team CI bot (server-team-bot) wrote :

PASSED: Continuous integration, rev:feb96661189ea0cef2910a6faad1bf918ef57ec9
https://jenkins.ubuntu.com/server/job/git-ubuntu-ci/488/
Executed test runs:
    SUCCESS: VM Setup
    SUCCESS: Build
    SUCCESS: Unit Tests
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/git-ubuntu-ci/488//rebuild

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

√ python3 setup.py build
√ python3 setup.py check
√ ./snap-wrappers/wrappers/git-ubuntu-self-test
  √ PASS (syntax) source-package-walker.py
  √ PASS (compilation) source-package-walker.py
  √ PASS (syntax) import-source-packages.py
  √ PASS (compilation) import-source-packages.py
  √ PASS (syntax) update-repository-alias.py
  √ PASS (compilation) update-repository-alias.py
  √ 335 passed, 3 xfailed in 183.72 seconds
  √ Coverage for importer.py at 64%
  √ Overall coverage 54%
√ Per-patch code review
  √ d87a1adecd324cd8fab3bf02c62fbff2352c1f64
    + Makes routine automagically determine target branch
    + LGTM
  √ ade437105ceece98173e13eae60fab431297f9c8
    + importppa.py also has a couple launchpad_versions_published_after()
      calls, however this is slated for removal soon. Otherwise, these
      calls would need adjusted similarly to importer.py.
    + Otherwise LGTM
  √ 371c75514e53ec4acc63c3fd48f3a31b3954e5ea
    + docstrings always welcome
    + LGTM
  √ feb96661189ea0cef2910a6faad1bf918ef57ec9
    + Refactors handling of active_series_only to pre-loop condition as
      a constructed dist_priority dictionary.
    + Adds interleave_launchpad_versions_published_after() to interleave
      the debian and ubuntu versions.
    + Adds testcase for same.
    + This routine is used to build a list of source publications
      (spi's), which are sorted by heapq.merge() using a custom key
      constructed by lookup of date created from launchpad, and dist_priority.
    + Studied dict comprehension for dist_priority creation. Looks
      correct.
    + Studied _get_cached_lp_link() behavior
    + interleave_launchpad_versions_published_after() also needs return
      value documented,
      rtype: iterator
      returns: iterator over the sorted values
    + fake_guspi() doesn't have return documented -- but it's internal to
      a test case, so I don't think it really matters. It's obvious
      enough what it's doing.
    + fake_guspi() documents 'Any' for param type, which appears to mean
      the function's parameters are not type specific. So could specify
      either a datetime object or, as in this case, integers.

review: Approve
Revision history for this message
Robie Basak (racb) wrote :

Thank you for the review!

On Tue, Apr 21, 2020 at 01:08:49AM -0000, Bryce Harrington wrote:
> + importppa.py also has a couple launchpad_versions_published_after()
> calls, however this is slated for removal soon. Otherwise, these
> calls would need adjusted similarly to importer.py.

Good spot! Yes - I checked the other callers but as they all turned out
to be slated for removal I ignored them.

> + interleave_launchpad_versions_published_after() also needs return
> value documented,
> rtype: iterator
> returns: iterator over the sorted values

Added, thanks.

> + fake_guspi() doesn't have return documented -- but it's internal to
> a test case, so I don't think it really matters. It's obvious
> enough what it's doing.

I added it anyway, thanks.

> + fake_guspi() documents 'Any' for param type, which appears to mean
> the function's parameters are not type specific. So could specify
> either a datetime object or, as in this case, integers.

Right - to keep things simple I took advantage of duck typing and used
integers instead of datetime objects. It only matters that they are
compared for sorting purposes.

I've forced pushed an update that only adjusts the final commit for
comments and docstrings, including a couple of additional minor
corrections to comments and docstrings that I don't think warrant
further review.

I will merge this branch. Thanks again!

Revision history for this message
Server Team CI bot (server-team-bot) wrote :

PASSED: Continuous integration, rev:7a70527cca95da0ef5f2d8aaf565e6441a663558
https://jenkins.ubuntu.com/server/job/git-ubuntu-ci/490/
Executed test runs:
    SUCCESS: VM Setup
    SUCCESS: Build
    SUCCESS: Unit Tests
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/git-ubuntu-ci/490//rebuild

review: Approve (continuous-integration)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/gitubuntu/git_repository.py b/gitubuntu/git_repository.py
2index ce7ebe8..6673878 100644
3--- a/gitubuntu/git_repository.py
4+++ b/gitubuntu/git_repository.py
5@@ -1669,6 +1669,13 @@ class GitUbuntuRepository:
6 """Extract the last version in debian/changelog of all
7 '<namespace>/<head_prefix>/debian/*' and
8 '<namespace>/<head_prefix>/ubuntu/*' branches.
9+
10+ :rtype: dict
11+ :returns: a dictionary keyed by branch name (without a 'refs/heads/'
12+ prefix). The value is a dictionary with two keys: 'head' and
13+ 'version'. The 'head' value is a pygit2.Branch object. The
14+ 'version' value is the package version string found at that branch
15+ head.
16 """
17 versions = dict()
18 errors = False
19diff --git a/gitubuntu/importer.py b/gitubuntu/importer.py
20index 604b478..12d09cb 100644
21--- a/gitubuntu/importer.py
22+++ b/gitubuntu/importer.py
23@@ -289,7 +289,7 @@ def _main_with_repo(
24
25 parent_overrides = parse_parentfile(parentfile, pkgname)
26
27- only_debian, history_found = import_publishes(
28+ history_found = import_publishes(
29 repo=repo,
30 pkgname=pkgname,
31 namespace=namespace,
32@@ -377,14 +377,17 @@ def _main_with_repo(
33 # Changelog notes go to refs/notes/commits due to LP: #1871838
34 'refs/notes/%s/changelog:refs/notes/commits' % namespace,
35 ])
36- # update our reference
37+ # Update HEAD on LP to point to our desired default; this is
38+ # ubuntu/devel if that exists, or debian/sid otherwise.
39+ if ('refs/heads/%s/ubuntu/devel' % namespace) in repo.references:
40+ desired_lp_default_branch = 'refs/heads/ubuntu/devel'
41+ else:
42+ desired_lp_default_branch = 'refs/heads/debian/sid'
43 lp_git_repo = lp.git_repositories.getByPath(path=repo_path)
44 for i in range(retries):
45 try:
46- if only_debian:
47- lp_git_repo.default_branch = 'refs/heads/debian/sid'
48- else:
49- lp_git_repo.default_branch = 'refs/heads/ubuntu/devel'
50+ logging.debug('Setting LP HEAD to %s' % desired_lp_default_branch)
51+ lp_git_repo.default_branch = desired_lp_default_branch
52 lp_git_repo.lp_save()
53 break
54 except (NotFound, PreconditionFailed) as e:
55@@ -1995,7 +1998,6 @@ def import_publishes(
56 parent_overrides,
57 ):
58 history_found = False
59- only_debian = False
60 srcpkg_information = None
61 if patches_applied:
62 _namespace = namespace
63@@ -2012,46 +2014,52 @@ def import_publishes(
64 import_unapplied_spi,
65 skip_orig=skip_orig,
66 )
67- for distname, versions, dist_sinfo in (
68- ("debian", debian_head_versions, debian_sinfo),
69- ("ubuntu", ubuntu_head_versions, ubuntu_sinfo)):
70- if active_series_only and distname == "debian":
71- continue
72- try:
73- for srcpkg_information in dist_sinfo.launchpad_versions_published_after(
74- versions,
75- namespace,
76- workdir=workdir,
77- active_series_only=active_series_only
78- ):
79- history_found = True
80- import_func(
81- repo=repo,
82- spi=srcpkg_information,
83- namespace=_namespace,
84- parent_overrides=parent_overrides,
85- )
86- except NoPublicationHistoryException:
87- logging.warning("No publication history found for %s in %s.",
88- pkgname, distname
89+ # Always look in Ubuntu for new publications
90+ gusi_head_versions_tuple_list = [
91+ (ubuntu_sinfo, ubuntu_head_versions),
92+ ]
93+ # Unless --active-series-only was specified, also look in Debian for new
94+ # publications.
95+ if not active_series_only:
96+ # For hash stability, we must process new Debian publications with the
97+ # same date_created before new Ubuntu publications, so we insert the
98+ # Debian entry before the Ubuntu entry.
99+ gusi_head_versions_tuple_list.insert(
100+ 0,
101+ (debian_sinfo, debian_head_versions),
102+ )
103+ try:
104+ # We must process new publications in order of date_created interleaved
105+ # across both distributions (Debian first) in order to maintain hash
106+ # stability according to the spec.
107+ for srcpkg_information in GitUbuntuSourceInformation.interleave_launchpad_versions_published_after(
108+ gusi_head_versions_tuple_list,
109+ namespace=namespace,
110+ workdir=workdir,
111+ active_series_only=active_series_only,
112+ ):
113+ history_found = True
114+ import_func(
115+ repo=repo,
116+ spi=srcpkg_information,
117+ namespace=_namespace,
118+ parent_overrides=parent_overrides,
119 )
120- if distname == 'ubuntu':
121- only_debian = True
122- except Exception as e:
123- if srcpkg_information is None:
124- msg = 'Unable to import %s to %s' % (import_type, distname)
125- else:
126- msg = 'Unable to import %s %s to %s' % (import_type,
127- str(srcpkg_information.version), distname)
128- if not patches_applied:
129- raise GitUbuntuImportError(msg) from e
130- else:
131- logging.error(msg)
132- logging.error(traceback.format_exc())
133+ except Exception as e:
134+ if srcpkg_information is None:
135+ msg = 'Unable to import %s' % import_type
136 else:
137- history_found = True
138+ msg = 'Unable to import %s %s' % (import_type,
139+ str(srcpkg_information.version))
140+ if not patches_applied:
141+ raise GitUbuntuImportError(msg) from e
142+ else:
143+ logging.error(msg)
144+ logging.error(traceback.format_exc())
145+ else:
146+ history_found = True
147
148- return (only_debian, history_found)
149+ return history_found
150
151
152 def parse_args(subparsers=None, base_subparsers=None):
153diff --git a/gitubuntu/source_information.py b/gitubuntu/source_information.py
154index 38df33e..cf729b8 100644
155--- a/gitubuntu/source_information.py
156+++ b/gitubuntu/source_information.py
157@@ -1,4 +1,5 @@
158 import functools
159+import heapq
160 import logging
161 import os
162 import re
163@@ -544,12 +545,38 @@ class GitUbuntuSourceInformation(object):
164 for srcpkg in spph:
165 yield self.get_corrected_spi(srcpkg, workdir)
166
167- def launchpad_versions_published_after(self, head_versions, namespace, workdir=None, active_series_only=False):
168+ def launchpad_versions_published_after(
169+ self,
170+ head_versions,
171+ namespace,
172+ workdir=None,
173+ active_series_only=False,
174+ ):
175+ """Return a sequence of GitUbuntuSourcePackageInformation instances
176+
177+ Return a sequence of GitUbuntuSourcePackageInformation instances
178+ representing Launchpad publications created in a Launchpad distribution
179+ after a particular point, as determined by the head_versions parameter.
180+
181+ :param dict head_versions: as returned by
182+ GitUbuntuRepository.get_heads_and_versions(). This may be empty, in
183+ which case all publications are returned. Otherwise, publications
184+ are skipped that match a version already found in head_versions, or
185+ with a date_created older than what is found in head_versions, is
186+ skipped, as well as any publications that have a prior date_created
187+ date.
188+ :param str namespace: the namespace prefix used in the git repository.
189+ :param workdir: passed through to the GitUbuntuSourcePackageInformation
190+ constructor.
191+ :param bool active_series_only: for Ubuntu, skip any series that is not
192+ an active series, as determined by self.active_series_name_list.
193+ :rtype: sequence(GitUbuntuSourcePackageInformation)
194+ """
195 args = {
196- 'exact_match':True,
197- 'source_name':self.pkgname,
198- 'order_by_date':True,
199- }
200+ 'exact_match': True,
201+ 'source_name': self.pkgname,
202+ 'order_by_date': True,
203+ }
204
205 # we have the date of the commit too, so we can double-check
206 # that it matches
207@@ -570,8 +597,10 @@ class GitUbuntuSourceInformation(object):
208 # Sanity check that the passed in srcpkg name has a publication
209 # history
210 if len(spph) == 0:
211- raise NoPublicationHistoryException("Is %s published in %s?" %
212- (self.pkgname, self.dist_name))
213+ logging.warning("No publication history found for %s in %s.",
214+ self.pkgname, self.dist_name
215+ )
216+ return
217 if len(head_versions) > 0:
218 _spph = list()
219 for spphr in spph:
220@@ -610,6 +639,87 @@ class GitUbuntuSourceInformation(object):
221 yield spi
222 raise StopIteration()
223
224+ @staticmethod
225+ def interleave_launchpad_versions_published_after(
226+ gusi_head_versions_tuple_list,
227+ namespace,
228+ workdir=None,
229+ active_series_only=False,
230+ ):
231+ """Interleave multiple calls to launchpad_versions_published_after
232+
233+ Interleave the result of multiple calls to the
234+ launchpad_versions_published_after() method. The specification requires
235+ new publications to be processed across the Debian and Ubuntu
236+ distributions in order of date_created to establish
237+ hash stability. launchpad_versions_published_after() returns the result
238+ of one distribution at a time. This method calls any number
239+ simultaneously and interleaves the results so that the caller sees a
240+ single combined sequence in ascending order of date_created. If
241+ date_created is the same, results are provided in the order that the
242+ distributions are specified in gusi_head_versions_tuple_list. In other
243+ words, the distribution ordering is the secondary sort key.
244+
245+ :param list(tuple(GitUbuntuSourceInformation, dict))
246+ gusi_head_versions_tuple_list: a list of the parameters to use for
247+ the underlying launchpad_versions_published_after() method call.
248+ The GitUbuntuSourceInformation instance is the object to call the
249+ method against. The second element of the tuple is the
250+ head_versions parameter to pass to that method. The other
251+ parameters are passed through to the underlying call as-is.
252+ :param namespace: passed through to
253+ launchpad_versions_published_after().
254+ :param workdir: passed through to launchpad_versions_published_after().
255+ :param bool active_series_only: passed through to
256+ launchpad_versions_published_after().
257+ :rtype: sequence(GitUbuntuSourcePackageInformation)
258+ :returns: the combined return sequences of the underlying calls to
259+ launchpad_versions_published_after() interleaved as specified.
260+ """
261+ # Create a mapping of distribution to priority, where distribution is
262+ # the Launchpad distribution_link string representing the distribution,
263+ # and priority is an integer (low number=high priority). Then we will
264+ # be able to sort according to priority as a secondary key to ensure
265+ # that if timestamps of a publication are the same across
266+ # distributions, the distribution mentioned first in
267+ # gusi_head_versions_tuple_list will appear in the results first.
268+ dist_priority = {
269+ gusi.dist.self_link: i
270+ for i, (gusi, head_version)
271+ in enumerate(gusi_head_versions_tuple_list)
272+ }
273+
274+ # Now that we have a mapping of distribution_link to priority, the sort
275+ # key function is simple: sort on date created first, and if they are
276+ # equal then use the distribution priority. Python will do this for us
277+ # if we provide a tuple. The distribution priority is necessary because
278+ # there is no guarantee that heapq.merge(), as used below, provides a
279+ # stable sort. If it did, then keying on date_created only would be
280+ # sufficient.
281+ def key_func(spi):
282+ distro_series = _get_cached_lp_link(spi._spphr, 'distro_series')
283+ distribution_link = distro_series.distribution_link
284+ return spi._spphr.date_created, dist_priority[distribution_link]
285+
286+ # Create one generator per gusi_head_versions_tuple_list entry.
287+ spi_generators_to_interleave = [
288+ gusi.launchpad_versions_published_after(
289+ head_versions=head_versions,
290+ namespace=namespace,
291+ workdir=workdir,
292+ active_series_only=active_series_only,
293+ )
294+ for gusi, head_versions in gusi_head_versions_tuple_list
295+ ]
296+
297+ # heapq.merge() will now do the interleaving. This relies on the
298+ # results of each of the calls to launchpad_versions_published_after()
299+ # already being sorted by date_created.
300+ return heapq.merge(
301+ *spi_generators_to_interleave,
302+ key=key_func,
303+ )
304+
305 def parse_pullfile(self, pullfile):
306 """Extract source file overrides from a file
307
308diff --git a/gitubuntu/source_information_test.py b/gitubuntu/source_information_test.py
309index 7aacb1a..7e88d77 100644
310--- a/gitubuntu/source_information_test.py
311+++ b/gitubuntu/source_information_test.py
312@@ -1,7 +1,10 @@
313 import collections
314+import functools
315+from unittest.mock import Mock, sentinel
316
317 import keyring
318 import pytest
319+import gitubuntu.source_information as target
320 from gitubuntu.source_information import derive_codename_from_series
321 from gitubuntu import source_information
322
323@@ -150,3 +153,124 @@ def test_derive_codename_from_broken_distro_info(monkeypatch, series, codename):
324 monkeypatch.setattr(source_information, "_ddi", MockDebianDistroInfo())
325
326 assert derive_codename_from_series(series) == codename
327+
328+
329+@pytest.mark.parametrize(['debian_input', 'ubuntu_input', 'expected'], [
330+ (
331+ [],
332+ [],
333+ [],
334+ ),
335+ (
336+ [0],
337+ [0],
338+ [(0, 'debian'), (0, 'ubuntu')],
339+ ),
340+ (
341+ [1],
342+ [0],
343+ [(0, 'ubuntu'), (1, 'debian')],
344+ ),
345+])
346+def test_interleave_launchpad_versions_published_after(
347+ debian_input,
348+ ubuntu_input,
349+ expected,
350+):
351+ """Test that interleaving works correctly
352+
353+ We will always ask for Debian before Ubuntu. When date_created is the same,
354+ Debian should come first. If date_created is different, the earlier
355+ date_created should come first.
356+
357+ :param list(Any) debian_input: the date_created attributes of the fake
358+ Launchpad source_package_publishing_history objects of the fake
359+ GitUbuntuSourcePackageInformation objects that will be returned by the
360+ faked call to launchpad_versions_published_after() call, for the Debian
361+ distribution.
362+ :param list(Any) debian_input: the date_created attributes of the fake
363+ Launchpad source_package_publishing_history objects of the fake
364+ GitUbuntuSourcePackageInformation objects that will be returned by the
365+ faked call to launchpad_versions_published_after() call, for the Ubuntu
366+ distribution.
367+ :param list(tuple(date_created, distribution)): the expected ordering in
368+ the return sequence of the
369+ interleave_launchpad_versions_published_after() call. date_created
370+ corresponds to the input data from debian_input and ubuntu_input.
371+ distribution is either the 'debian' string or the 'ubuntu' string
372+ depending on whether the return value was supposed to come from the
373+ Debian or Ubuntu fake GitUbuntuSourceInformation object.
374+ """
375+ # Store all fake guspis created, since equivalent guspis will be asserted
376+ # for equality later, and Mock objects that are otherwise identical do not
377+ # compare equal if they are separate instances. For the same call to
378+ # fake_guspi() below, we want to return the same Mock object instance so
379+ # that they do compare equal.
380+ fake_guspis = {}
381+ def fake_guspi(dist_link, date_created):
382+ """Return a fake target.GitUbuntuSourcePackageInformation object
383+
384+ Multiple calls to this function with the same parameters return the
385+ same fake objects so that they compare equal.
386+
387+ :param Any dist_link: a mock distribution_link that can be compared
388+ against later (for example, use a unittest.mock.sentinel).
389+ :param Any date_created: the fake date_created attribute of the
390+ underlying Launchpad source_package_publishing_history object.
391+ :rtype: Mock
392+ :returns: the fake target.GitUbuntuSourcePackageInformation object.
393+ """
394+ try:
395+ return fake_guspis[(dist_link, date_created)]
396+ except KeyError:
397+ result = Mock(
398+ spec=target.GitUbuntuSourcePackageInformation,
399+ _spphr=Mock(
400+ distro_series_link=str(hash(dist_link)) + '_distro_series',
401+ distro_series=Mock(distribution_link=dist_link),
402+ date_created=date_created,
403+ )
404+ )
405+ fake_guspis[(dist_link, date_created)] = result
406+ return result
407+
408+ # A fake target.GitUbuntuSourceInformation object to represent the Debian
409+ # distribution.
410+ debian = Mock(
411+ spec=target.GitUbuntuSourceInformation,
412+ dist=Mock(self_link=sentinel.debian_dist_link),
413+ launchpad_versions_published_after=Mock(return_value=[
414+ fake_guspi(sentinel.debian_dist_link, date_created)
415+ for date_created in debian_input
416+ ])
417+ )
418+ # A fake target.GitUbuntuSourceInformation object to represent the Ubuntu
419+ # distribution.
420+ ubuntu = Mock(
421+ spec=target.GitUbuntuSourceInformation,
422+ dist=Mock(self_link=sentinel.ubuntu_dist_link),
423+ launchpad_versions_published_after=Mock(return_value=[
424+ fake_guspi(sentinel.ubuntu_dist_link, date_created)
425+ for date_created in ubuntu_input
426+ ])
427+ )
428+
429+ gusi_head_versions_tuple_list = [
430+ (debian, None),
431+ (ubuntu, None),
432+ ]
433+ result = target.GitUbuntuSourceInformation.interleave_launchpad_versions_published_after(
434+ gusi_head_versions_tuple_list=gusi_head_versions_tuple_list,
435+ namespace=None,
436+ )
437+ expanded_result = list(result)
438+ assert expanded_result == [
439+ fake_guspi(
440+ dist_link={
441+ 'debian': sentinel.debian_dist_link,
442+ 'ubuntu': sentinel.ubuntu_dist_link,
443+ }[dist],
444+ date_created=date_created,
445+ )
446+ for date_created, dist in expected
447+ ]

Subscribers

People subscribed via source and target branches