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
diff --git a/ppa/ppa.py b/ppa/ppa.py
index e055dc4..57ec14b 100755
--- a/ppa/ppa.py
+++ b/ppa/ppa.py
@@ -22,6 +22,7 @@ from lazr.restfulclient.errors import BadRequest, NotFound, Unauthorized
22from .constants import URL_AUTOPKGTEST22from .constants import URL_AUTOPKGTEST
23from .io import open_url23from .io import open_url
24from .job import (get_waiting, get_running)24from .job import (get_waiting, get_running)
25from .result import get_results
2526
2627
27class PendingReason(enum.Enum):28class PendingReason(enum.Enum):
@@ -555,6 +556,30 @@ class Ppa:
555 return get_running(response, releases=releases, ppa=str(self))556 return get_running(response, releases=releases, ppa=str(self))
556 return []557 return []
557558
559 def get_autopkgtest_results(self, releases, architectures):
560 """Returns iterator of results from autopkgtest runs for this PPA.
561
562 See get_results() for details
563
564 :param list[str] releases: The Ubuntu series codename(s), or None.
565 :param list[str] architectures: The hardware architectures.
566 :rtype: Iterator[dict]
567 :returns: Autopkgtest results, if any, or an empty list on error
568 """
569 results = []
570 for release in releases:
571 base_results_fmt = f"{URL_AUTOPKGTEST}/results/autopkgtest-%s-%s-%s/"
572 base_results_url = base_results_fmt % (release, self.owner_name, self.name)
573 response = open_url(f"{base_results_url}?format=plain")
574 if response:
575 trigger_sets = {}
576 for result in get_results(response, base_results_url, arches=architectures):
577 trigger = ', '.join([str(r) for r in result.get_triggers()])
578 trigger_sets.setdefault(trigger, [])
579 trigger_sets[trigger].append(result)
580 results.append(trigger_sets)
581 return results
582
558583
559def ppa_address_split(ppa_address):584def ppa_address_split(ppa_address):
560 """Parse an address for a PPA into its owner and name components.585 """Parse an address for a PPA into its owner and name components.
diff --git a/ppa/result.py b/ppa/result.py
index fe47621..d0afa95 100755
--- a/ppa/result.py
+++ b/ppa/result.py
@@ -340,7 +340,7 @@ if __name__ == "__main__":
340 print("Loading live excuses data")340 print("Loading live excuses data")
341 print("-------------------------")341 print("-------------------------")
342 base_results_fmt = f"{URL_AUTOPKGTEST}/results/autopkgtest-%s-%s-%s/"342 base_results_fmt = f"{URL_AUTOPKGTEST}/results/autopkgtest-%s-%s-%s/"
343 base_results_url = base_results_fmt % ('kinetic', 'bryce', 'nginx-merge-v1.22.0-1')343 base_results_url = base_results_fmt % ('mantic', 'bryce', 'apache2-merge-v2.4.54-3')
344 url = f"{base_results_url}?format=plain"344 url = f"{base_results_url}?format=plain"
345 response = open_url(url)345 response = open_url(url)
346346
diff --git a/scripts/ppa b/scripts/ppa
index c4af301..9dbb15a 100755
--- a/scripts/ppa
+++ b/scripts/ppa
@@ -71,12 +71,10 @@ from ppa.constants import (
71 ARCHES_PPA_ALL,71 ARCHES_PPA_ALL,
72 ARCHES_PPA_DEFAULT,72 ARCHES_PPA_DEFAULT,
73 ARCHES_AUTOPKGTEST,73 ARCHES_AUTOPKGTEST,
74 URL_AUTOPKGTEST,
75 LOCAL_REPOSITORY_PATH,74 LOCAL_REPOSITORY_PATH,
76 LOCAL_REPOSITORY_MIRRORING_DIRECTIONS,75 LOCAL_REPOSITORY_MIRRORING_DIRECTIONS,
77)76)
78from ppa.dict import unpack_to_dict77from ppa.dict import unpack_to_dict
79from ppa.io import open_url
80from ppa.job import show_waiting, show_running78from ppa.job import show_waiting, show_running
81from ppa.lp import Lp79from ppa.lp import Lp
82from ppa.ppa import (80from ppa.ppa import (
@@ -88,7 +86,7 @@ from ppa.ppa import (
88)86)
89from ppa.ppa_group import PpaGroup, PpaAlreadyExists87from ppa.ppa_group import PpaGroup, PpaAlreadyExists
90from ppa.repository import Repository88from ppa.repository import Repository
91from ppa.result import get_results, show_results89from ppa.result import show_results
92from ppa.text import o2str, ansi_hyperlink90from ppa.text import o2str, ansi_hyperlink
93from ppa.trigger import Trigger91from ppa.trigger import Trigger
9492
@@ -865,7 +863,7 @@ def command_tests(lp: Lp, config: dict[str, str]) -> int:
865863
866 try:864 try:
867 # Triggers865 # Triggers
868 print("* Triggers:")866 source_pub_triggers = []
869 for source_pub in the_ppa.get_source_publications():867 for source_pub in the_ppa.get_source_publications():
870 series_codename = source_pub.distro_series.name868 series_codename = source_pub.distro_series.name
871 if series_codename not in releases:869 if series_codename not in releases:
@@ -874,9 +872,6 @@ def command_tests(lp: Lp, config: dict[str, str]) -> int:
874 if packages and (pkg not in packages):872 if packages and (pkg not in packages):
875 continue873 continue
876 ver = source_pub.source_package_version874 ver = source_pub.source_package_version
877 url = f"https://launchpad.net/ubuntu/+source/{pkg}/{ver}"
878 source_hyperlink = ansi_hyperlink(url, f"{pkg}/{ver}")
879 print(f" - Source {source_hyperlink}: {source_pub.status}")
880 triggers = [Trigger(pkg, ver, arch, series_codename, the_ppa) for arch in architectures]875 triggers = [Trigger(pkg, ver, arch, series_codename, the_ppa) for arch in architectures]
881876
882 if config.get("show_rdepends"):877 if config.get("show_rdepends"):
@@ -910,7 +905,16 @@ def command_tests(lp: Lp, config: dict[str, str]) -> int:
910 for arch905 for arch
911 in architectures906 in architectures
912 ])907 ])
908 source_pub_triggers.append((source_pub, triggers))
913909
910 # Display triggers
911 print("* Triggers:")
912 for source_pub, triggers in source_pub_triggers:
913 pkg = source_pub.source_package_name
914 ver = source_pub.source_package_version
915 url = f"https://launchpad.net/ubuntu/+source/{pkg}/{ver}"
916 source_hyperlink = ansi_hyperlink(url, f"{pkg}/{ver}")
917 print(f" - Source {source_hyperlink}: {source_pub.status}")
914 if config.get("show_urls"):918 if config.get("show_urls"):
915 for trigger in triggers:919 for trigger in triggers:
916 title = ''920 title = ''
@@ -937,25 +941,11 @@ def command_tests(lp: Lp, config: dict[str, str]) -> int:
937 trigger.action_url + "&all-proposed=1",941 trigger.action_url + "&all-proposed=1",
938 f"Trigger all-proposed {title}@{trigger.arch}💍"942 f"Trigger all-proposed {title}@{trigger.arch}💍"
939 )943 )
940 print(f" + " + pad.join([basic_trig, all_proposed_trig]))944 print(" + " + pad.join([basic_trig, all_proposed_trig]))
941945
942 # Results946 # Results
943 ## get_autopkgtest_results947 show_results(the_ppa.get_autopkgtest_results(releases, architectures),
944 results = []948 config.get('show_urls'))
945 for release in releases:
946 base_results_fmt = f"{URL_AUTOPKGTEST}/results/autopkgtest-%s-%s-%s/"
947 base_results_url = base_results_fmt % (release, the_ppa.owner_name, the_ppa.name)
948 url = f"{base_results_url}?format=plain"
949 response = open_url(url)
950 if response:
951 trigger_sets = {}
952 for result in get_results(response, base_results_url, arches=architectures):
953 trigger = ', '.join([str(r) for r in result.get_triggers()])
954 trigger_sets.setdefault(trigger, [])
955 trigger_sets[trigger].append(result)
956 results.append(trigger_sets)
957
958 show_results(results, config.get('show_urls'))
959949
960 # Running Queue950 # Running Queue
961 show_running(sorted(the_ppa.get_autopkgtest_running(releases),951 show_running(sorted(the_ppa.get_autopkgtest_running(releases),
diff --git a/tests/test_ppa.py b/tests/test_ppa.py
index 7e2e4bf..1f8fc65 100644
--- a/tests/test_ppa.py
+++ b/tests/test_ppa.py
@@ -8,28 +8,42 @@
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"""Ppa class tests"""
12
11import os13import os
12import sys14import sys
15import time
1316
14import pytest17import pytest
18from mock import patch
1519
16sys.path.insert(0, os.path.realpath(20sys.path.insert(0, os.path.realpath(
17 os.path.join(os.path.dirname(os.path.realpath(__file__)), "..")))21 os.path.join(os.path.dirname(os.path.realpath(__file__)), "..")))
1822
23from ppa.constants import URL_AUTOPKGTEST
19from ppa.ppa import Ppa, ppa_address_split, get_ppa24from ppa.ppa import Ppa, ppa_address_split, get_ppa
25from ppa.result import Result
2026
2127
22def test_object():28def test_object():
23 """Check that PPA objects can be instantiated."""29 """Check that PPA objects can be instantiated."""
24 ppa = Ppa('test-ppa-name', 'test-owner-name')30 ppa = Ppa('test-ppa-name', 'test-owner-name', ppa_description='test-desc', service='test-svc')
25 assert ppa31 assert ppa
32 assert ppa.ppa_name == 'test-ppa-name'
33 assert ppa.owner_name == 'test-owner-name'
34 assert ppa.ppa_description == 'test-desc'
2635
2736
28def test_description():37def test_repr():
29 """Check specifying a description when creating a PPA."""38 """Check Ppa object representation."""
30 ppa = Ppa('test-ppa-name', 'test-owner-name', 'test-description')39 ppa = Ppa('a', 'b', 'c', 'd')
40 assert repr(ppa) == "Ppa(ppa_name='a', owner_name='b')"
3141
32 assert 'test-description' in ppa.ppa_description42
43def test_str():
44 """Check Ppa object textual presentation."""
45 ppa = Ppa('a', 'b', 'c', 'd')
46 assert f"{ppa}" == 'b/a'
3347
3448
35def test_address():49def test_address():
@@ -38,6 +52,65 @@ def test_address():
38 assert ppa.address == "ppa:owner/test"52 assert ppa.address == "ppa:owner/test"
3953
4054
55def test_description():
56 """Check specifying a description when creating a PPA."""
57 ppa = Ppa('test-ppa-name', 'test-owner-name', 'test-description')
58
59 assert 'test-description' in ppa.ppa_description
60
61
62@pytest.mark.parametrize('releases, architectures, expected_num_results, expected_num_triggers', [
63 ([], [], 0, 0),
64 (['x'], ['amd64'], 1, 1),
65 (['x', 'y', 'z'], ['amd64'], 3, 1),
66 (['x'], ['amd64', 'armhf', 'i386'], 1, 3),
67 (['x', 'y', 'z'], ['amd64', 'armhf', 'i386'], 3, 3),
68])
69@patch('ppa.ppa.get_results')
70@patch('ppa.ppa.open_url')
71def test_get_autopkgtest_results(mock_open_url, mock_get_results,
72 releases, architectures, expected_num_results, expected_num_triggers):
73 """Check that autopkgtest results are retrieved and iterated correctly."""
74 owner_name = 'a'
75 ppa_name = 'b'
76
77 # Bypass open_url() entirely so we don't try to retrieve anything.
78 # Usually this returns a response that if valid is then passed to
79 # get_results(), but since we'll be patching get_results(), all that
80 # matters is that validity check passes. We can do that by having
81 # our mock open_url return a generic valid value, True.
82 mock_open_url.return_value = True
83
84 # Substitute in our fake results to be returned by get_results().
85 # We need to have semi-valid looking URLs to pass internal checks,
86 # but can provide trivially fake data.
87 fake_results = {}
88 fake_data_url = "https://fake.data"
89 timestamp = time.strptime('20030201_040506', "%Y%m%d_%H%M%S")
90 for release in releases:
91 k = f"{URL_AUTOPKGTEST}/results/autopkgtest-{release}-{owner_name}-{ppa_name}/"
92 fake_results[k] = []
93 for arch in architectures:
94 # We don't care about the triggers for the result, just the
95 # number of results and their basic identity, so replace the
96 # get_triggers() call to avoid it making any remote calls.
97 result = Result(url=fake_data_url, time=timestamp, series=release, arch=arch, source=None)
98 result.get_triggers = lambda: "x"
99 fake_results[k].append(result)
100
101 def fake_get_results(response, url, arches):
102 return fake_results[url]
103 mock_get_results.side_effect = fake_get_results
104
105 ppa = Ppa(ppa_name, owner_name)
106 results = ppa.get_autopkgtest_results(releases, architectures)
107
108 assert len(results) == expected_num_results
109 for trigger_set in results:
110 assert isinstance(trigger_set, dict)
111 assert all(len(triggers) == expected_num_triggers for triggers in trigger_set.values())
112
113
41@pytest.mark.parametrize('address, expected', [114@pytest.mark.parametrize('address, expected', [
42 # Successful cases115 # Successful cases
43 ('bb', (None, 'bb')),116 ('bb', (None, 'bb')),
diff --git a/tests/test_result.py b/tests/test_result.py
index 4063b83..96b1535 100644
--- a/tests/test_result.py
+++ b/tests/test_result.py
@@ -28,8 +28,15 @@ from ppa.result import Result
2828
29def test_object():29def test_object():
30 """Checks that Result objects can be instantiated."""30 """Checks that Result objects can be instantiated."""
31 result = Result('url', None, 'b', 'c', 'd')31 timestamp = time.strptime('20030201_040506', "%Y%m%d_%H%M%S")
32 result = Result('url', timestamp, 'b', 'c', 'd')
32 assert result33 assert result
34 assert result.url == 'url'
35 assert result.time == timestamp
36 assert result.series == 'b'
37 assert result.arch == 'c'
38 assert result.source == 'd'
39 assert not result.error_message
3340
3441
35def test_repr():42def test_repr():

Subscribers

People subscribed via source and target branches

to all changes: