Merge ppa-dev-tools:refactor-autopkgtest-results into ppa-dev-tools:main

Proposed by Bryce Harrington
Status: Merged
Merged at revision: 6c63268fdbe01cbd7ca456a0eac6c2313116f551
Proposed branch: ppa-dev-tools:refactor-autopkgtest-results
Merge into: ppa-dev-tools:main
Diff against target: 286 lines (+126/-31)
5 files modified
ppa/ppa.py (+25/-0)
ppa/result.py (+1/-1)
scripts/ppa (+14/-24)
tests/test_ppa.py (+78/-5)
tests/test_result.py (+8/-1)
Reviewer Review Type Date Requested Status
Andreas Hasenack (community) Approve
PpaDevTools Developers Pending
Canonical Server Pending
Canonical Server Reporter Pending
Review via email: mp+449980@code.launchpad.net

Description of the change

Refactors logic out of the tests command relating to Autopkgtest results into a new
get_autopkgtest_results() routine, with a corresponding test case.

The test case was the most challenging part, but I feel its finally adequate. I plan to add some functionality for filtering and refining the test results displayed when running 'ppa tests <ppa>' so that you can limit to particular packages, releases, etc. However, before getting into that I wanted a solid test to base the work on. In implementing this I can see some additional refactoring that could make the test more concise and narrowly focused, however I'll leave that to the planned followup work.

In addition to this commit, I'm including several testing-related refinements that feel like they go along with this.

To post a comment you must log in.
Revision history for this message
Andreas Hasenack (ahasenack) wrote :

+1, just two minor optional comments.

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

Thanks, updated and landed:

stirling: ~/src/PpaDevTools/ppa-dev-tools-feature-rdepends-testing$ git merge --ff-only refactor-autopkgtest-results
Updating c56a856..6c63268
Fast-forward
 ppa/ppa.py | 25 +++++++++++++++++++++++++
 ppa/result.py | 2 +-
 scripts/ppa | 38 ++++++++++++++------------------------
 tests/test_ppa.py | 83 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
 tests/test_result.py | 9 ++++++++-
 5 files changed, 126 insertions(+), 31 deletions(-)
stirling: ~/src/PpaDevTools/ppa-dev-tools-feature-rdepends-testing$ git push
Total 0 (delta 0), reused 0 (delta 0), pack-reused 0
To git+ssh://git.launchpad.net/ppa-dev-tools
   c56a856..6c63268 main -> main

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/ppa/ppa.py b/ppa/ppa.py
2index e055dc4..57ec14b 100755
3--- a/ppa/ppa.py
4+++ b/ppa/ppa.py
5@@ -22,6 +22,7 @@ from lazr.restfulclient.errors import BadRequest, NotFound, Unauthorized
6 from .constants import URL_AUTOPKGTEST
7 from .io import open_url
8 from .job import (get_waiting, get_running)
9+from .result import get_results
10
11
12 class PendingReason(enum.Enum):
13@@ -555,6 +556,30 @@ class Ppa:
14 return get_running(response, releases=releases, ppa=str(self))
15 return []
16
17+ def get_autopkgtest_results(self, releases, architectures):
18+ """Returns iterator of results from autopkgtest runs for this PPA.
19+
20+ See get_results() for details
21+
22+ :param list[str] releases: The Ubuntu series codename(s), or None.
23+ :param list[str] architectures: The hardware architectures.
24+ :rtype: Iterator[dict]
25+ :returns: Autopkgtest results, if any, or an empty list on error
26+ """
27+ results = []
28+ for release in releases:
29+ base_results_fmt = f"{URL_AUTOPKGTEST}/results/autopkgtest-%s-%s-%s/"
30+ base_results_url = base_results_fmt % (release, self.owner_name, self.name)
31+ response = open_url(f"{base_results_url}?format=plain")
32+ if response:
33+ trigger_sets = {}
34+ for result in get_results(response, base_results_url, arches=architectures):
35+ trigger = ', '.join([str(r) for r in result.get_triggers()])
36+ trigger_sets.setdefault(trigger, [])
37+ trigger_sets[trigger].append(result)
38+ results.append(trigger_sets)
39+ return results
40+
41
42 def ppa_address_split(ppa_address):
43 """Parse an address for a PPA into its owner and name components.
44diff --git a/ppa/result.py b/ppa/result.py
45index fe47621..d0afa95 100755
46--- a/ppa/result.py
47+++ b/ppa/result.py
48@@ -340,7 +340,7 @@ if __name__ == "__main__":
49 print("Loading live excuses data")
50 print("-------------------------")
51 base_results_fmt = f"{URL_AUTOPKGTEST}/results/autopkgtest-%s-%s-%s/"
52- base_results_url = base_results_fmt % ('kinetic', 'bryce', 'nginx-merge-v1.22.0-1')
53+ base_results_url = base_results_fmt % ('mantic', 'bryce', 'apache2-merge-v2.4.54-3')
54 url = f"{base_results_url}?format=plain"
55 response = open_url(url)
56
57diff --git a/scripts/ppa b/scripts/ppa
58index c4af301..9dbb15a 100755
59--- a/scripts/ppa
60+++ b/scripts/ppa
61@@ -71,12 +71,10 @@ from ppa.constants import (
62 ARCHES_PPA_ALL,
63 ARCHES_PPA_DEFAULT,
64 ARCHES_AUTOPKGTEST,
65- URL_AUTOPKGTEST,
66 LOCAL_REPOSITORY_PATH,
67 LOCAL_REPOSITORY_MIRRORING_DIRECTIONS,
68 )
69 from ppa.dict import unpack_to_dict
70-from ppa.io import open_url
71 from ppa.job import show_waiting, show_running
72 from ppa.lp import Lp
73 from ppa.ppa import (
74@@ -88,7 +86,7 @@ from ppa.ppa import (
75 )
76 from ppa.ppa_group import PpaGroup, PpaAlreadyExists
77 from ppa.repository import Repository
78-from ppa.result import get_results, show_results
79+from ppa.result import show_results
80 from ppa.text import o2str, ansi_hyperlink
81 from ppa.trigger import Trigger
82
83@@ -865,7 +863,7 @@ def command_tests(lp: Lp, config: dict[str, str]) -> int:
84
85 try:
86 # Triggers
87- print("* Triggers:")
88+ source_pub_triggers = []
89 for source_pub in the_ppa.get_source_publications():
90 series_codename = source_pub.distro_series.name
91 if series_codename not in releases:
92@@ -874,9 +872,6 @@ def command_tests(lp: Lp, config: dict[str, str]) -> int:
93 if packages and (pkg not in packages):
94 continue
95 ver = source_pub.source_package_version
96- url = f"https://launchpad.net/ubuntu/+source/{pkg}/{ver}"
97- source_hyperlink = ansi_hyperlink(url, f"{pkg}/{ver}")
98- print(f" - Source {source_hyperlink}: {source_pub.status}")
99 triggers = [Trigger(pkg, ver, arch, series_codename, the_ppa) for arch in architectures]
100
101 if config.get("show_rdepends"):
102@@ -910,7 +905,16 @@ def command_tests(lp: Lp, config: dict[str, str]) -> int:
103 for arch
104 in architectures
105 ])
106+ source_pub_triggers.append((source_pub, triggers))
107
108+ # Display triggers
109+ print("* Triggers:")
110+ for source_pub, triggers in source_pub_triggers:
111+ pkg = source_pub.source_package_name
112+ ver = source_pub.source_package_version
113+ url = f"https://launchpad.net/ubuntu/+source/{pkg}/{ver}"
114+ source_hyperlink = ansi_hyperlink(url, f"{pkg}/{ver}")
115+ print(f" - Source {source_hyperlink}: {source_pub.status}")
116 if config.get("show_urls"):
117 for trigger in triggers:
118 title = ''
119@@ -937,25 +941,11 @@ def command_tests(lp: Lp, config: dict[str, str]) -> int:
120 trigger.action_url + "&all-proposed=1",
121 f"Trigger all-proposed {title}@{trigger.arch}💍"
122 )
123- print(f" + " + pad.join([basic_trig, all_proposed_trig]))
124+ print(" + " + pad.join([basic_trig, all_proposed_trig]))
125
126 # Results
127- ## get_autopkgtest_results
128- results = []
129- for release in releases:
130- base_results_fmt = f"{URL_AUTOPKGTEST}/results/autopkgtest-%s-%s-%s/"
131- base_results_url = base_results_fmt % (release, the_ppa.owner_name, the_ppa.name)
132- url = f"{base_results_url}?format=plain"
133- response = open_url(url)
134- if response:
135- trigger_sets = {}
136- for result in get_results(response, base_results_url, arches=architectures):
137- trigger = ', '.join([str(r) for r in result.get_triggers()])
138- trigger_sets.setdefault(trigger, [])
139- trigger_sets[trigger].append(result)
140- results.append(trigger_sets)
141-
142- show_results(results, config.get('show_urls'))
143+ show_results(the_ppa.get_autopkgtest_results(releases, architectures),
144+ config.get('show_urls'))
145
146 # Running Queue
147 show_running(sorted(the_ppa.get_autopkgtest_running(releases),
148diff --git a/tests/test_ppa.py b/tests/test_ppa.py
149index 7e2e4bf..1f8fc65 100644
150--- a/tests/test_ppa.py
151+++ b/tests/test_ppa.py
152@@ -8,28 +8,42 @@
153 # Released under GNU GPLv2 or later, read the file 'LICENSE.GPLv2+' for
154 # more information.
155
156+"""Ppa class tests"""
157+
158 import os
159 import sys
160+import time
161
162 import pytest
163+from mock import patch
164
165 sys.path.insert(0, os.path.realpath(
166 os.path.join(os.path.dirname(os.path.realpath(__file__)), "..")))
167
168+from ppa.constants import URL_AUTOPKGTEST
169 from ppa.ppa import Ppa, ppa_address_split, get_ppa
170+from ppa.result import Result
171
172
173 def test_object():
174 """Check that PPA objects can be instantiated."""
175- ppa = Ppa('test-ppa-name', 'test-owner-name')
176+ ppa = Ppa('test-ppa-name', 'test-owner-name', ppa_description='test-desc', service='test-svc')
177 assert ppa
178+ assert ppa.ppa_name == 'test-ppa-name'
179+ assert ppa.owner_name == 'test-owner-name'
180+ assert ppa.ppa_description == 'test-desc'
181
182
183-def test_description():
184- """Check specifying a description when creating a PPA."""
185- ppa = Ppa('test-ppa-name', 'test-owner-name', 'test-description')
186+def test_repr():
187+ """Check Ppa object representation."""
188+ ppa = Ppa('a', 'b', 'c', 'd')
189+ assert repr(ppa) == "Ppa(ppa_name='a', owner_name='b')"
190
191- assert 'test-description' in ppa.ppa_description
192+
193+def test_str():
194+ """Check Ppa object textual presentation."""
195+ ppa = Ppa('a', 'b', 'c', 'd')
196+ assert f"{ppa}" == 'b/a'
197
198
199 def test_address():
200@@ -38,6 +52,65 @@ def test_address():
201 assert ppa.address == "ppa:owner/test"
202
203
204+def test_description():
205+ """Check specifying a description when creating a PPA."""
206+ ppa = Ppa('test-ppa-name', 'test-owner-name', 'test-description')
207+
208+ assert 'test-description' in ppa.ppa_description
209+
210+
211+@pytest.mark.parametrize('releases, architectures, expected_num_results, expected_num_triggers', [
212+ ([], [], 0, 0),
213+ (['x'], ['amd64'], 1, 1),
214+ (['x', 'y', 'z'], ['amd64'], 3, 1),
215+ (['x'], ['amd64', 'armhf', 'i386'], 1, 3),
216+ (['x', 'y', 'z'], ['amd64', 'armhf', 'i386'], 3, 3),
217+])
218+@patch('ppa.ppa.get_results')
219+@patch('ppa.ppa.open_url')
220+def test_get_autopkgtest_results(mock_open_url, mock_get_results,
221+ releases, architectures, expected_num_results, expected_num_triggers):
222+ """Check that autopkgtest results are retrieved and iterated correctly."""
223+ owner_name = 'a'
224+ ppa_name = 'b'
225+
226+ # Bypass open_url() entirely so we don't try to retrieve anything.
227+ # Usually this returns a response that if valid is then passed to
228+ # get_results(), but since we'll be patching get_results(), all that
229+ # matters is that validity check passes. We can do that by having
230+ # our mock open_url return a generic valid value, True.
231+ mock_open_url.return_value = True
232+
233+ # Substitute in our fake results to be returned by get_results().
234+ # We need to have semi-valid looking URLs to pass internal checks,
235+ # but can provide trivially fake data.
236+ fake_results = {}
237+ fake_data_url = "https://fake.data"
238+ timestamp = time.strptime('20030201_040506', "%Y%m%d_%H%M%S")
239+ for release in releases:
240+ k = f"{URL_AUTOPKGTEST}/results/autopkgtest-{release}-{owner_name}-{ppa_name}/"
241+ fake_results[k] = []
242+ for arch in architectures:
243+ # We don't care about the triggers for the result, just the
244+ # number of results and their basic identity, so replace the
245+ # get_triggers() call to avoid it making any remote calls.
246+ result = Result(url=fake_data_url, time=timestamp, series=release, arch=arch, source=None)
247+ result.get_triggers = lambda: "x"
248+ fake_results[k].append(result)
249+
250+ def fake_get_results(response, url, arches):
251+ return fake_results[url]
252+ mock_get_results.side_effect = fake_get_results
253+
254+ ppa = Ppa(ppa_name, owner_name)
255+ results = ppa.get_autopkgtest_results(releases, architectures)
256+
257+ assert len(results) == expected_num_results
258+ for trigger_set in results:
259+ assert isinstance(trigger_set, dict)
260+ assert all(len(triggers) == expected_num_triggers for triggers in trigger_set.values())
261+
262+
263 @pytest.mark.parametrize('address, expected', [
264 # Successful cases
265 ('bb', (None, 'bb')),
266diff --git a/tests/test_result.py b/tests/test_result.py
267index 4063b83..96b1535 100644
268--- a/tests/test_result.py
269+++ b/tests/test_result.py
270@@ -28,8 +28,15 @@ from ppa.result import Result
271
272 def test_object():
273 """Checks that Result objects can be instantiated."""
274- result = Result('url', None, 'b', 'c', 'd')
275+ timestamp = time.strptime('20030201_040506', "%Y%m%d_%H%M%S")
276+ result = Result('url', timestamp, 'b', 'c', 'd')
277 assert result
278+ assert result.url == 'url'
279+ assert result.time == timestamp
280+ assert result.series == 'b'
281+ assert result.arch == 'c'
282+ assert result.source == 'd'
283+ assert not result.error_message
284
285
286 def test_repr():

Subscribers

People subscribed via source and target branches

to all changes: