Merge ppa-dev-tools:improve-ppa-address-handling into ppa-dev-tools:main

Proposed by Bryce Harrington
Status: Merged
Merge reported by: Bryce Harrington
Merged at revision: 8b8bb77ba229832de75930972b29b6a0aeca7b1a
Proposed branch: ppa-dev-tools:improve-ppa-address-handling
Merge into: ppa-dev-tools:main
Diff against target: 958 lines (+331/-225)
15 files modified
README.md (+2/-2)
ppa/ppa.py (+85/-37)
ppa/ppa_group.py (+21/-14)
scripts/ppa (+27/-37)
tests/helpers.py (+90/-0)
tests/test_io.py (+5/-2)
tests/test_job.py (+6/-15)
tests/test_lp.py (+2/-1)
tests/test_ppa.py (+48/-5)
tests/test_ppa_group.py (+29/-87)
tests/test_result.py (+1/-1)
tests/test_scripts_ppa.py (+1/-13)
tests/test_subtest.py (+1/-1)
tests/test_trigger.py (+1/-1)
tests/test_version.py (+12/-9)
Reviewer Review Type Date Requested Status
Lena Voytek (community) Approve
Canonical Server Pending
Canonical Server Reporter Pending
Review via email: mp+431612@code.launchpad.net

Description of the change

Fixes some reported bugs relating to handling of the ppa-address argument to various commands.

This improves handling of cases where no team is specified (and thus should default appropriately to the current user's namespace), and catching a variety of invalid team and ppa names. A slew of test cases are added for this.

Of note, this also adds the ability to pass in a PPA url, in addition to a plain name or a formal ppa:foo/bar style address. I think this should make the commands a bit more handy.

To post a comment you must log in.
8b8bb77... by Bryce Harrington

README: Use formal ppa address in docs

With the recent fix, all the steps that were documented in README should
now work as written. However, using the formal ppa address is more
generally correct so should be preferred in the entry-level docs.

Revision history for this message
Lena Voytek (lvoytek) wrote :

LGTM, just some small code cleanup comments. All logic looks good

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

I've incorporated fixes for all the feedback (and filed a bug for one), and landed the branch:

To git+ssh://git.launchpad.net/ppa-dev-tools
   222405e..e5b49d1 main -> main

Thank you again for the review, Lena!

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/README.md b/README.md
index 4b5d34d..d26e1a4 100644
--- a/README.md
+++ b/README.md
@@ -43,7 +43,7 @@ $ dput ppa:my-name/my-ppa some-package.changes
43Wait until all packages in the PPA have finished building43Wait until all packages in the PPA have finished building
4444
45```45```
46$ ppa wait my-ppa46$ ppa wait ppa:my-name/my-ppa
47```47```
4848
49Set the public description for a PPA from a file49Set the public description for a PPA from a file
@@ -55,5 +55,5 @@ $ cat some-package/README | ppa desc ppa:my-name/my-ppa
55Delete the PPA55Delete the PPA
5656
57```57```
58$ ppa destroy my-ppa58$ ppa destroy ppa:my-name/my-ppa
59```59```
diff --git a/ppa/ppa.py b/ppa/ppa.py
index 2617346..888df48 100755
--- a/ppa/ppa.py
+++ b/ppa/ppa.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
11import re
12
11from textwrap import indent13from textwrap import indent
12from functools import lru_cache14from functools import lru_cache
13from lazr.restfulclient.errors import BadRequest, NotFound15from lazr.restfulclient.errors import BadRequest, NotFound
@@ -17,7 +19,8 @@ class PpaDoesNotExist(BaseException):
17 """Exception indicating a requested PPA could not be found."""19 """Exception indicating a requested PPA could not be found."""
1820
19 def __init__(self, ppa_name, team_name, message=None):21 def __init__(self, ppa_name, team_name, message=None):
20 """22 """Initializes the exception object.
23
21 :param str ppa_name: The name of the missing PPA.24 :param str ppa_name: The name of the missing PPA.
22 :param str message: An error message.25 :param str message: An error message.
23 """26 """
@@ -36,40 +39,6 @@ class PpaDoesNotExist(BaseException):
36 return f"The PPA '{self.ppa_name}' does not exist for team or user '{self.team_name}'"39 return f"The PPA '{self.ppa_name}' does not exist for team or user '{self.team_name}'"
3740
3841
39def ppa_address_split(ppa_address, default_team='me'):
40 """Parse an address for a PPA into its team and name components
41 """
42 if ppa_address.startswith('ppa:'):
43 if '/' not in ppa_address:
44 return (None, None)
45 rem = ppa_address.split('ppa:', 1)[1]
46 team_name = rem.split('/', 1)[0]
47 ppa_name = rem.split('/', 1)[1]
48 else:
49 team_name = default_team
50 ppa_name = ppa_address
51 return (team_name, ppa_name)
52
53
54def get_das(distro, series_name, arch_name):
55 """Retrive the arch-series for the given distro.
56
57 :param distribution distro: The Launchpad distribution object.
58 :param str series_name: The distro's codename for the series.
59 :param str arch_name: The hardware architecture.
60 :rtype: distro_arch_series
61 :returns: A Launchpad distro_arch_series object, or None on error.
62 """
63 if series_name is None or series_name == '':
64 return None
65
66 for series in distro.series:
67 if series.name != series_name:
68 continue
69 return series.getDistroArchSeries(archtag=arch_name)
70 return None
71
72
73class Ppa:42class Ppa:
74 """Encapsulates data needed to access and conveniently wrap a PPA43 """Encapsulates data needed to access and conveniently wrap a PPA
7544
@@ -77,7 +46,7 @@ class Ppa:
77 of data from the remote.46 of data from the remote.
78 """47 """
79 def __init__(self, ppa_name, team_name, ppa_description=None, service=None):48 def __init__(self, ppa_name, team_name, ppa_description=None, service=None):
80 """Initializes a new Ppa object for a given PPA49 """Initializes a new Ppa object for a given PPA.
8150
82 This creates only the local representation of the PPA, it does51 This creates only the local representation of the PPA, it does
83 not cause a new PPA to be created in Launchpad. For that, see52 not cause a new PPA to be created in Launchpad. For that, see
@@ -102,6 +71,11 @@ class Ppa:
102 self._service = service71 self._service = service
10372
104 def __repr__(self):73 def __repr__(self):
74 """Machine-parsable unique representation of object.
75
76 :rtype: str
77 :returns: Official string representation of the object.
78 """
105 return (f'{self.__class__.__name__}('79 return (f'{self.__class__.__name__}('
106 f'ppa_name={self.ppa_name!r}, team_name={self.team_name!r})')80 f'ppa_name={self.ppa_name!r}, team_name={self.team_name!r})')
10781
@@ -130,7 +104,7 @@ class Ppa:
130 @property104 @property
131 @lru_cache105 @lru_cache
132 def address(self):106 def address(self):
133 """The proper identifier of the PPA107 """The proper identifier of the PPA.
134108
135 :rtype: str109 :rtype: str
136 :returns: The full identification string for the PPA.110 :returns: The full identification string for the PPA.
@@ -371,3 +345,77 @@ class Ppa:
371 if not retval:345 if not retval:
372 print("Successfully published all builds for all architectures")346 print("Successfully published all builds for all architectures")
373 return retval347 return retval
348
349
350def ppa_address_split(ppa_address, default_team=None):
351 """Parse an address for a PPA into its team and name components.
352
353 :param str ppa_address: A ppa name or address.
354 :param str default_team: (Optional) name of team to use if missing.
355 :rtype: tuple(str, str)
356 :returns: The team name and ppa name as a tuple, or (None, None) on error.
357 """
358 if not ppa_address or len(ppa_address)<2:
359 return (None, None)
360 if ppa_address.startswith('ppa:'):
361 if '/' not in ppa_address:
362 return (None, None)
363 rem = ppa_address.split('ppa:', 1)[1]
364 team_name = rem.split('/', 1)[0]
365 ppa_name = rem.split('/', 1)[1]
366 elif ppa_address.startswith('http'):
367 # Only launchpad PPA urls are supported
368 m = re.search(r'https:\/\/launchpad\.net\/~([^/]+)\/\+archive\/ubuntu\/(.+)$', ppa_address)
369 if not m:
370 return (None, None)
371 team_name = m.group(1)
372 ppa_name = m.group(2)
373 elif '/' in ppa_address:
374 team_name = ppa_address.split('/', 1)[0]
375 ppa_name = ppa_address.split('/', 1)[1]
376 else:
377 team_name = default_team
378 ppa_name = ppa_address
379
380 if (team_name and ppa_name
381 and not (any(x.isupper() for x in team_name))
382 and not (any(x.isupper() for x in ppa_name))
383 and ppa_name.isascii()
384 and '/' not in ppa_name
385 and len(ppa_name)>1):
386 return (team_name, ppa_name)
387
388 return (None, None)
389
390
391def get_das(distro, series_name, arch_name):
392 """Retrive the arch-series for the given distro.
393
394 :param distribution distro: The Launchpad distribution object.
395 :param str series_name: The distro's codename for the series.
396 :param str arch_name: The hardware architecture.
397 :rtype: distro_arch_series
398 :returns: A Launchpad distro_arch_series object, or None on error.
399 """
400 if series_name is None or series_name == '':
401 return None
402
403 for series in distro.series:
404 if series.name != series_name:
405 continue
406 return series.getDistroArchSeries(archtag=arch_name)
407 return None
408
409
410def get_ppa(lp, config):
411 """Load the specified PPA from Launchpad
412
413 :param Lp lp: The Launchpad wrapper object.
414 :param dict config: Configuration param:value map.
415 :rtype: Ppa
416 :returns: Specified PPA as a Ppa object.
417 """
418 return Ppa(
419 ppa_name=config.get('ppa_name', None),
420 team_name=config.get('team_name', None),
421 service=lp)
diff --git a/ppa/ppa_group.py b/ppa/ppa_group.py
index 1a9c8b7..0160574 100755
--- a/ppa/ppa_group.py
+++ b/ppa/ppa_group.py
@@ -16,10 +16,11 @@ from lazr.restfulclient.errors import BadRequest
1616
1717
18class PpaAlreadyExists(BaseException):18class PpaAlreadyExists(BaseException):
19 '''Exception indicating a PPA operation could not be performed'''19 """Exception indicating a PPA operation could not be performed."""
2020
21 def __init__(self, ppa_name, message=None):21 def __init__(self, ppa_name, message=None):
22 """22 """Initializes the exception object.
23
23 :param str ppa_name: The name of the pre-existing PPA.24 :param str ppa_name: The name of the pre-existing PPA.
24 :param str message: An error message.25 :param str message: An error message.
25 """26 """
@@ -44,25 +45,31 @@ class PpaGroup:
44 This class provides a proxy object for interacting with collections45 This class provides a proxy object for interacting with collections
45 of PPA.46 of PPA.
46 """47 """
47 def __init__(self, service, name='me'):48 def __init__(self, service, name):
48 """49 """Initializes a new PpaGroup object for a named person or team.
50
49 :param launchpadlib.service service: The Launchpad service object.51 :param launchpadlib.service service: The Launchpad service object.
50 :param str name: Launchpad username52 :param str name: Launchpad team or user name
51 """53 """
52 assert(service is not None)54 assert service is not None
53 self.service = service55 self.service = service
5456 self.name = name
55 if name == 'me':
56 me = self.service.me
57 self.name = me.name
58 else:
59 self.name = name
6057
61 def __repr__(self):58 def __repr__(self):
59 """Machine-parsable unique representation of object.
60
61 :rtype: str
62 :returns: Official string representation of the object.
63 """
62 return (f'{self.__class__.__name__}('64 return (f'{self.__class__.__name__}('
63 f'service={self.service!r}, name={self.name!r})')65 f'service={self.service!r}, name={self.name!r})')
6466
65 def __str__(self):67 def __str__(self):
68 """Human-readable summary of the object.
69
70 :rtype: str
71 :returns: Printable summary of the object.
72 """
66 return 'tbd'73 return 'tbd'
6774
68 @property75 @property
@@ -70,8 +77,8 @@ class PpaGroup:
70 def team(self):77 def team(self):
71 """The team that owns this collection of PPAs.78 """The team that owns this collection of PPAs.
7279
73 :rtype: tbd80 :rtype: launchpadlib.person
74 :returns: tbd81 :returns: Launchpad person object that owns this PPA.
75 """82 """
76 return self.service.people[self.name]83 return self.service.people[self.name]
7784
diff --git a/scripts/ppa b/scripts/ppa
index 361d91b..6965d44 100755
--- a/scripts/ppa
+++ b/scripts/ppa
@@ -77,7 +77,12 @@ from ppa.job import (
77 show_running77 show_running
78)78)
79from ppa.lp import Lp79from ppa.lp import Lp
80from ppa.ppa import Ppa, PpaDoesNotExist80from ppa.ppa import (
81 get_ppa,
82 ppa_address_split,
83 Ppa,
84 PpaDoesNotExist
85)
81from ppa.ppa_group import PpaGroup, PpaAlreadyExists86from ppa.ppa_group import PpaGroup, PpaAlreadyExists
82from ppa.result import (87from ppa.result import (
83 Result,88 Result,
@@ -87,7 +92,7 @@ from ppa.text import o2str
87from ppa.trigger import Trigger92from ppa.trigger import Trigger
8893
89import ppa.debug94import ppa.debug
90from ppa.debug import dbg, die, warn95from ppa.debug import dbg, warn
9196
9297
93def UNIMPLEMENTED():98def UNIMPLEMENTED():
@@ -197,7 +202,7 @@ def create_arg_parser():
197 return parser202 return parser
198203
199204
200def create_config(args):205def create_config(lp, args):
201 """Creates config object by loading from file and adding args.206 """Creates config object by loading from file and adding args.
202207
203 This routine merges the command line parameter values with data208 This routine merges the command line parameter values with data
@@ -208,6 +213,7 @@ def create_config(args):
208 This permits setting static values in the config file(s), and using213 This permits setting static values in the config file(s), and using
209 the command line args for variable settings and overrides.214 the command line args for variable settings and overrides.
210215
216 :param launchpadlib.service lp: The Launchpad service object.
211 :param Namespace args: The parsed args from ArgumentParser.217 :param Namespace args: The parsed args from ArgumentParser.
212 :rtype: dict218 :rtype: dict
213 :returns: dict of configuration parameters and values, or None on error219 :returns: dict of configuration parameters and values, or None on error
@@ -233,21 +239,12 @@ def create_config(args):
233 for k, v in DEFAULT_CONFIG.items():239 for k, v in DEFAULT_CONFIG.items():
234 config.setdefault(k, v)240 config.setdefault(k, v)
235241
236 # TODO: function to convert string to ppa_name, team_name242 lp_username = None
237 if args.ppa_name.startswith('ppa:'):243 if lp.me:
238 if '/' not in args.ppa_name:244 lp_username = lp.me.name
239 die("Invalid ppa name '{}'".format(args.ppa_name))245 config['team_name'], config['ppa_name'] = ppa_address_split(args.ppa_name, lp_username)
240 rem = args.ppa_name.split('ppa:', 1)[1]246 if not config['team_name'] or not config['ppa_name']:
241 config['team_name'] = rem.split('/', 1)[0]247 warn("Invalid ppa name '{}'".format(args.ppa_name))
242 config['ppa_name'] = rem.split('/', 1)[1]
243 elif '/' in args.ppa_name:
244 config['team_name'] = args.ppa_name.split('/', 1)[0]
245 config['ppa_name'] = args.ppa_name.split('/', 1)[1]
246 else:
247 config['ppa_name'] = args.ppa_name
248 # TODO: Set team_name to current lp_username if available
249 if 'ppa_name' not in config or 'team_name' not in config:
250 warn("Unknown ppa or team name")
251 return None248 return None
252249
253 if args.dry_run:250 if args.dry_run:
@@ -263,20 +260,6 @@ def create_config(args):
263 return config260 return config
264261
265262
266def get_ppa(lp, config):
267 """Load the specified PPA from Launchpad
268
269 :param Lp lp: The Launchpad wrapper object.
270 :param dict config: Configuration param:value map.
271 :rtype: Ppa
272 :returns: Specified PPA as a Ppa object.
273 """
274 return Ppa(
275 ppa_name=config.get('ppa_name', None),
276 team_name=config.get('team_name', None),
277 service=lp)
278
279
280################263################
281### Commands ###264### Commands ###
282################265################
@@ -296,12 +279,13 @@ def command_create(lp, config):
296279
297 ppa_name = config.get('ppa_name')280 ppa_name = config.get('ppa_name')
298 if not ppa_name:281 if not ppa_name:
299 warn("Could not determine ppa_name")282 warn("Could not determine PPA name")
300 return os.EX_USAGE283 return os.EX_USAGE
301284
302 team_name = config.get('team_name')285 team_name = config.get('team_name')
303 if not team_name:286 if not team_name:
304 team_name = 'me'287 warn("Could not determine team name")
288 return os.EX_USAGE
305289
306 architectures = config.get('architectures', ARCHES_PPA)290 architectures = config.get('architectures', ARCHES_PPA)
307291
@@ -404,8 +388,13 @@ def command_list(lp, config, filter_func=None):
404 if not lp:388 if not lp:
405 return 1389 return 1
406390
391 team_name = config.get('team_name')
392 if not team_name:
393 warn("Could not determine team name")
394 return os.EX_USAGE
395
407 try:396 try:
408 ppa_group = PpaGroup(service=lp)397 ppa_group = PpaGroup(service=lp, name=team_name)
409 for ppa in ppa_group.ppas:398 for ppa in ppa_group.ppas:
410 print(ppa.address)399 print(ppa.address)
411 return os.EX_OK400 return os.EX_OK
@@ -629,8 +618,9 @@ def main(args):
629 """618 """
630 config = create_config(args)619 config = create_config(args)
631 if not config:620 if not config:
632 parser.error("Invalid parameters")621 return os.EX_CONFIG
633 return 1622
623 ppa.debug.DEBUGGING = config.get('debug', False)
634 dbg("Configuration:")624 dbg("Configuration:")
635 dbg(config)625 dbg(config)
636626
diff --git a/tests/helpers.py b/tests/helpers.py
637new file mode 100644627new file mode 100644
index 0000000..c774cfc
--- /dev/null
+++ b/tests/helpers.py
@@ -0,0 +1,90 @@
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3
4# Author: Bryce Harrington <bryce@canonical.com>
5#
6# Copyright (C) 2019 Bryce W. Harrington
7#
8# Released under GNU GPLv2 or later, read the file 'LICENSE.GPLv2+' for
9# more information.
10
11import os
12import sys
13
14sys.path.insert(0, os.path.realpath(
15 os.path.join(os.path.dirname(os.path.realpath(__file__)), "..")))
16
17from ppa.ppa import Ppa
18from ppa.ppa_group import PpaGroup, PpaAlreadyExists
19
20
21class PersonMock:
22 """A stand-in for a Launchpad Person object."""
23 def __init__(self, name):
24 self.name = name
25 self._ppas = []
26
27 def createPPA(self, name, description, displayname):
28 for ppa in self._ppas:
29 if ppa.name == name:
30 raise PpaAlreadyExists(name)
31 new_ppa = Ppa(name, self.name, description)
32 self._ppas.append(new_ppa)
33 return True
34
35 def lp_save(self):
36 return True
37
38 @property
39 def ppas(self):
40 return self._ppas
41
42
43class LaunchpadMock:
44 """A stand-in for Launchpad."""
45 def __init__(self):
46 self.people = { 'me': PersonMock('me') }
47
48 def add_person(self, name):
49 print("Adding person %s" %(name))
50 self.people[name] = PersonMock(name)
51
52 @property
53 def me(self):
54 return self.people['me']
55
56
57class LpServiceMock:
58 """A stand-in for the Lp service object."""
59 def __init__(self):
60 self.launchpad = LaunchpadMock()
61
62 @property
63 def me(self):
64 return self.launchpad.people['me']
65
66 @property
67 def people(self):
68 return self.launchpad.people
69
70 def get_bug(self, bug_id):
71 class BugMock:
72 @property
73 def title(self):
74 return "Mock bug report"
75
76 @property
77 def description(self):
78 return "Description line 1\n\ndescription line 2"
79
80 return BugMock()
81
82
83class RequestResponseMock:
84 """A stand-in for a request result."""
85 def __init__(self, text):
86 self._text = text.encode('utf-8')
87
88 def read(self):
89 """Simply returns the exact text provided in initializer."""
90 return self._text
diff --git a/tests/test_io.py b/tests/test_io.py
index 57f8737..c15d2af 100644
--- a/tests/test_io.py
+++ b/tests/test_io.py
@@ -15,13 +15,16 @@ import sys
15import urllib15import urllib
1616
17sys.path.insert(0, os.path.realpath(17sys.path.insert(0, os.path.realpath(
18 os.path.join(os.path.dirname(__file__), "..")))18 os.path.join(os.path.dirname(os.path.realpath(__file__)), "..")))
1919
20from ppa.io import open_url20from ppa.io import open_url
2121
2222
23def test_open_url(tmp_path):23def test_open_url(tmp_path):
24 """Checks that the open_url() object reads from a valid URL."""24 """Checks that the open_url() object reads from a valid URL.
25
26 :param fixture tmp_path: Temp dir.
27 """
25 f = tmp_path / "open_url.txt"28 f = tmp_path / "open_url.txt"
26 f.write_text("abcde")29 f.write_text("abcde")
2730
diff --git a/tests/test_job.py b/tests/test_job.py
index 44491c2..78a12c7 100644
--- a/tests/test_job.py
+++ b/tests/test_job.py
@@ -14,19 +14,10 @@ import os
14import sys14import sys
1515
16sys.path.insert(0, os.path.realpath(16sys.path.insert(0, os.path.realpath(
17 os.path.join(os.path.dirname(__file__), "..")))17 os.path.join(os.path.dirname(os.path.realpath(__file__)), "..")))
1818
19from ppa.job import Job, get_running, get_waiting19from ppa.job import Job, get_running, get_waiting
2020from tests.helpers import RequestResponseMock
21
22class ResponseMock:
23 """Synthetic response object"""
24 def __init__(self, text):
25 self._text = text.encode('utf-8')
26
27 def read(self):
28 """Simply returns the exact text provided in initializer."""
29 return self._text
3021
3122
32def test_object():23def test_object():
@@ -81,7 +72,7 @@ def test_request_url():
8172
8273
83def test_get_running():74def test_get_running():
84 """Checks output from the get_running() command"""75 """Checks output from the get_running() command."""
85 json_text = ('{"mypackage": {"my-job-id": {"focal": { "arm64": ['76 json_text = ('{"mypackage": {"my-job-id": {"focal": { "arm64": ['
86 '{"submit-time": "2022-08-19 20:59:01", '77 '{"submit-time": "2022-08-19 20:59:01", '
87 '"triggers": ["yourpackage/1.2.3"], '78 '"triggers": ["yourpackage/1.2.3"], '
@@ -89,7 +80,7 @@ def test_get_running():
89 '1234, '80 '1234, '
90 '"Log Output Here"'81 '"Log Output Here"'
91 '] } } } }')82 '] } } } }')
92 fake_response = ResponseMock(json_text)83 fake_response = RequestResponseMock(json_text)
93 job = next(get_running(fake_response, series='focal', ppa='ppa:me/myppa'))84 job = next(get_running(fake_response, series='focal', ppa='ppa:me/myppa'))
94 assert repr(job) == "Job(source_package='mypackage', series='focal', arch='arm64')"85 assert repr(job) == "Job(source_package='mypackage', series='focal', arch='arm64')"
95 assert job.triggers == ["yourpackage/1.2.3"]86 assert job.triggers == ["yourpackage/1.2.3"]
@@ -97,7 +88,7 @@ def test_get_running():
9788
9889
99def test_get_waiting():90def test_get_waiting():
100 """Checks output from the get_waiting() command"""91 """Checks output from the get_waiting() command."""
101 # TODO: I think ppas need to be in "ppa" instead of under "ubuntu" but need to doublecheck.92 # TODO: I think ppas need to be in "ppa" instead of under "ubuntu" but need to doublecheck.
102 json_text = ('{ "ubuntu": { "focal": { "amd64": ['93 json_text = ('{ "ubuntu": { "focal": { "amd64": ['
103 ' "a\\n{\\"requester\\": \\"you\\",'94 ' "a\\n{\\"requester\\": \\"you\\",'
@@ -108,7 +99,7 @@ def test_get_waiting():
108 ' \\"ppas\\": [ \\"ppa:me/myppa\\" ],'99 ' \\"ppas\\": [ \\"ppa:me/myppa\\" ],'
109 ' \\"triggers\\": [ \\"c/3.2-1\\", \\"d/2-2\\" ] }"'100 ' \\"triggers\\": [ \\"c/3.2-1\\", \\"d/2-2\\" ] }"'
110 '] } } }')101 '] } } }')
111 fake_response = ResponseMock(json_text)102 fake_response = RequestResponseMock(json_text)
112 job = next(get_waiting(fake_response, series='focal', ppa='ppa:me/myppa'))103 job = next(get_waiting(fake_response, series='focal', ppa='ppa:me/myppa'))
113 assert job104 assert job
114 assert job.source_package == "b"105 assert job.source_package == "b"
diff --git a/tests/test_lp.py b/tests/test_lp.py
index d5978ce..98f4888 100644
--- a/tests/test_lp.py
+++ b/tests/test_lp.py
@@ -34,7 +34,8 @@ from mock import Mock
34import pytest34import pytest
35import logging35import logging
3636
37sys.path.insert(0, os.path.realpath(os.path.join(os.path.dirname(__file__), "..")))37sys.path.insert(0, os.path.realpath(
38 os.path.join(os.path.dirname(os.path.realpath(__file__)), "..")))
3839
39from ppa.lp import Lp40from ppa.lp import Lp
40from launchpadlib.launchpad import Launchpad41from launchpadlib.launchpad import Launchpad
diff --git a/tests/test_ppa.py b/tests/test_ppa.py
index a71336f..2410f83 100644
--- a/tests/test_ppa.py
+++ b/tests/test_ppa.py
@@ -11,26 +11,69 @@
11import os11import os
12import sys12import sys
1313
14import pytest
15
14sys.path.insert(0, os.path.realpath(16sys.path.insert(0, os.path.realpath(
15 os.path.join(os.path.dirname(__file__), "..")))17 os.path.join(os.path.dirname(os.path.realpath(__file__)), "..")))
1618
17from ppa.ppa import Ppa19from ppa.ppa import Ppa, ppa_address_split, get_ppa
1820
1921
20def test_object():22def test_object():
21 """Check that PPA objects can be instantiated"""23 """Check that PPA objects can be instantiated."""
22 ppa = Ppa('test-ppa-name', 'test-team-name')24 ppa = Ppa('test-ppa-name', 'test-team-name')
23 assert ppa25 assert ppa
2426
2527
26def test_description():28def test_description():
27 """Check specifying a description when creating a PPA"""29 """Check specifying a description when creating a PPA."""
28 ppa = Ppa('test-ppa-name', 'test-team-name', 'test-description')30 ppa = Ppa('test-ppa-name', 'test-team-name', 'test-description')
2931
30 assert 'test-description' in ppa.ppa_description32 assert 'test-description' in ppa.ppa_description
3133
3234
33def test_address():35def test_address():
34 """Check getting the PPA address"""36 """Check getting the PPA address."""
35 ppa = Ppa('test', 'team')37 ppa = Ppa('test', 'team')
36 assert ppa.address == "ppa:team/test"38 assert ppa.address == "ppa:team/test"
39
40
41@pytest.mark.parametrize('address, default_team, expected', [
42 # Successful cases
43 ('bb', 'me', ('me', 'bb')),
44 ('123', 'me', ('me', '123')),
45 ('a/123', 'me', ('a', '123')),
46 ('ppa:a/bb', None, ('a', 'bb')),
47 ('ppa:ç/bb', None, ('ç', 'bb')),
48 ('https://launchpad.net/~a/+archive/ubuntu/bb', None, ('a', 'bb')),
49
50 # Expected failure cases
51 ('ppa:', None, (None, None)),
52 (None, None, (None, None)),
53 ('', None, (None, None)),
54 ('/', None, (None, None)),
55 (':/', None, (None, None)),
56 ('////', None, (None, None)),
57 ('ppa:/', None, (None, None)),
58 ('ppa:a/', None, (None, None)),
59 ('ppa:/bb', None, (None, None)),
60 ('ppa:a/bç', None, (None, None)),
61 ('ppa:A/bb', None, (None, None)),
62 ('ppa/a/bb', None, (None, None)),
63 ('ppa:a/bb/c', None, (None, None)),
64 ('ppa:a/bB', None, (None, None)),
65 ('http://launchpad.net/~a/+archive/ubuntu/bb', None, (None, None)),
66 ('https://example.com/~a/+archive/ubuntu/bb', None, (None, None)),
67 ('https://launchpad.net/~a/+archive/nobuntu/bb', None, (None, None)),
68])
69def test_ppa_address_split(address, default_team, expected):
70 """Check ppa address input strings can be parsed properly."""
71 result = ppa_address_split(address, default_team=default_team)
72 assert result == expected
73
74
75def test_get_ppa():
76 ppa = get_ppa(None, {'team_name': 'a', 'ppa_name': 'bb'})
77 assert type(ppa) is Ppa
78 assert ppa.team_name == 'a'
79 assert ppa.ppa_name == 'bb'
diff --git a/tests/test_ppa_group.py b/tests/test_ppa_group.py
index e03a841..63907fd 100644
--- a/tests/test_ppa_group.py
+++ b/tests/test_ppa_group.py
@@ -14,141 +14,83 @@ import sys
14import pytest14import pytest
1515
16sys.path.insert(0, os.path.realpath(16sys.path.insert(0, os.path.realpath(
17 os.path.join(os.path.dirname(__file__), "..")))17 os.path.join(os.path.dirname(os.path.realpath(__file__)), "..")))
1818
19from ppa.ppa import Ppa19from ppa.ppa import Ppa
20from ppa.ppa_group import PpaGroup, PpaAlreadyExists20from ppa.ppa_group import PpaGroup, PpaAlreadyExists
2121from tests.helpers import PersonMock, LaunchpadMock, LpServiceMock
22
23class PersonMock:
24 def __init__(self, name):
25 self.name = name
26 self._ppas = []
27
28 def createPPA(self, name, description, displayname):
29 for ppa in self._ppas:
30 if ppa.name == name:
31 raise PpaAlreadyExists(name)
32 new_ppa = Ppa(name, self.name, description)
33 self._ppas.append(new_ppa)
34 return True
35
36 def lp_save(self):
37 return True
38
39 @property
40 def ppas(self):
41 return self._ppas
42
43
44class LaunchpadMock:
45 def __init__(self):
46 self.people = {'me': PersonMock('me')}
47
48 def add_person(self, name):
49 print(f"Adding person {name}")
50 self.people[name] = PersonMock(name)
51
52 @property
53 def me(self):
54 return self.people['me']
55
56
57class LpServiceMock:
58 def __init__(self):
59 self.launchpad = LaunchpadMock()
60
61 @property
62 def me(self):
63 return self.launchpad.people['me']
64
65 @property
66 def people(self):
67 return self.launchpad.people
68
69 def get_bug(self, bug_id):
70 class BugMock:
71 @property
72 def title(self):
73 return "Mock bug report"
74
75 @property
76 def description(self):
77 return "Description line 1\n\ndescription line 2"
78
79 return BugMock()
8022
8123
82def test_object():24def test_object():
83 """Checks that PpaGroup objects can be instantiated."""25 """Checks that PpaGroup objects can be instantiated."""
84 ppa_group = PpaGroup(service=LpServiceMock())26 ppa_group = PpaGroup(service=LpServiceMock(), name=None)
85 assert(ppa_group)27 assert ppa_group
8628
8729
88def test_create_ppa():30def test_create_ppa():
89 """Checks that PpaGroups can create PPAs"""31 """Checks that PpaGroups can create PPAs."""
90 name = 'test_ppa'32 name = 'test_ppa'
91 ppa_group = PpaGroup(service=LpServiceMock())33 ppa_group = PpaGroup(service=LpServiceMock(), name='me')
92 ppa = ppa_group.create(name)34 ppa = ppa_group.create(name)
93 assert(ppa is not None)35 assert ppa is not None
94 assert(name in ppa.address)36 assert name in ppa.address
95 assert(type(ppa.description) is str)37 assert type(ppa.description) is str
9638
9739
98def test_create_existing_ppa():40def test_create_existing_ppa():
99 """Check exception creating an already created PPA"""41 """Check exception creating an already created PPA."""
100 name = 'test_ppa'42 name = 'test_ppa'
101 ppa_group = PpaGroup(service=LpServiceMock())43 ppa_group = PpaGroup(service=LpServiceMock(), name='me')
102 ppa_group.create(name)44 ppa_group.create(name)
103 with pytest.raises(PpaAlreadyExists):45 with pytest.raises(PpaAlreadyExists):
104 ppa_group.create(name)46 ppa_group.create(name)
10547
10648
107def test_create_with_description():49def test_create_with_description():
108 """Check setting a description for a PPA"""50 """Check setting a description for a PPA."""
109 ppa_group = PpaGroup(service=LpServiceMock())51 ppa_group = PpaGroup(service=LpServiceMock(), name='me')
110 description = 'PPA Test Description'52 description = 'PPA Test Description'
111 ppa = ppa_group.create('test_ppa_with_description', description)53 ppa = ppa_group.create('test_ppa_with_description', description)
112 assert(ppa is not None)54 assert ppa is not None
113 assert(ppa.description == description)55 assert ppa.description == description
11456
11557
116def test_create_with_team():58def test_create_with_team():
117 """Check creating a PPA for a particular team"""59 """Check creating a PPA for a particular team."""
118 lp = LpServiceMock()60 lp = LpServiceMock()
119 lp.launchpad.add_person('test_team_name')61 lp.launchpad.add_person('test_team_name')
120 ppa_group = PpaGroup(service=lp, name='test_team_name')62 ppa_group = PpaGroup(service=lp, name='test_team_name')
121 ppa = ppa_group.create('ppa_test_name')63 ppa = ppa_group.create('ppa_test_name')
122 assert(ppa is not None)64 assert ppa is not None
123 assert(ppa.address == 'ppa:test_team_name/ppa_test_name')65 assert ppa.address == 'ppa:test_team_name/ppa_test_name'
12466
12567
126def test_create_for_lpbug():68def test_create_for_lpbug():
127 """Check associating a bug # when creating a PPA"""69 """Check associating a bug # when creating a PPA."""
128 ppa_group = PpaGroup(service=LpServiceMock())70 ppa_group = PpaGroup(service=LpServiceMock(), name='me')
129 lpbug = '1234567'71 lpbug = '1234567'
130 ppa = ppa_group.create('lp' + lpbug)72 ppa = ppa_group.create('lp' + lpbug)
131 assert(ppa is not None)73 assert ppa is not None
132 assert(lpbug in ppa.description)74 assert lpbug in ppa.description
13375
13476
135def test_create_for_merge_proposal():77def test_create_for_merge_proposal():
136 """Check associating a merge proposal when creating a PPA"""78 """Check associating a merge proposal when creating a PPA."""
137 ppa_group = PpaGroup(service=LpServiceMock())79 ppa_group = PpaGroup(service=LpServiceMock(), name='me')
138 version = '1.2.3-4'80 version = '1.2.3-4'
139 ppa = ppa_group.create('merge.' + version)81 ppa = ppa_group.create('merge.' + version)
140 assert(ppa is not None)82 assert ppa is not None
141 assert(version in ppa.description)83 assert version in ppa.description
14284
14385
144def test_list_ppas():86def test_list_ppas():
145 """Check listing the PPAs for a PPA group"""87 """Check listing the PPAs for a PPA group."""
146 test_ppa_list = ['a', 'b', 'c', 'd']88 test_ppa_list = ['a', 'b', 'c', 'd']
147 ppa_group = PpaGroup(service=LpServiceMock())89 ppa_group = PpaGroup(service=LpServiceMock(), name='me')
14890
149 # Add several ppas91 # Add several ppas
150 for ppa in test_ppa_list:92 for ppa in test_ppa_list:
151 ppa_group.create(ppa)93 ppa_group.create(ppa)
15294
153 ppas = [ppa.name for ppa in list(ppa_group.ppas)]95 ppas = [ppa.name for ppa in list(ppa_group.ppas)]
154 assert(test_ppa_list == ppas)96 assert test_ppa_list == ppas
diff --git a/tests/test_result.py b/tests/test_result.py
index cbd574c..8c498a4 100644
--- a/tests/test_result.py
+++ b/tests/test_result.py
@@ -17,7 +17,7 @@ import time
17import gzip17import gzip
1818
19sys.path.insert(0, os.path.realpath(19sys.path.insert(0, os.path.realpath(
20 os.path.join(os.path.dirname(__file__), "..")))20 os.path.join(os.path.dirname(os.path.realpath(__file__)), "..")))
21DATA_DIR = os.path.realpath(21DATA_DIR = os.path.realpath(
22 os.path.join(os.path.dirname(__file__), "data"))22 os.path.join(os.path.dirname(__file__), "data"))
2323
diff --git a/tests/test_scripts_ppa.py b/tests/test_scripts_ppa.py
index fed190b..7a578f7 100644
--- a/tests/test_scripts_ppa.py
+++ b/tests/test_scripts_ppa.py
@@ -19,7 +19,7 @@ import argparse
19import pytest19import pytest
2020
21SCRIPT_NAME = 'ppa'21SCRIPT_NAME = 'ppa'
22BASE_PATH = os.path.realpath(os.path.join(os.path.dirname(__file__), '..'))22BASE_PATH = os.path.realpath(os.path.join(os.path.dirname(os.path.realpath(__file__)), '..'))
23sys.path.insert(0, BASE_PATH)23sys.path.insert(0, BASE_PATH)
2424
25if '.pybuild' in BASE_PATH:25if '.pybuild' in BASE_PATH:
@@ -184,18 +184,6 @@ def test_create_config():
184 pass184 pass
185185
186186
187# TODO: Monkeypatch in Lp = MockLp
188@pytest.mark.xfail(reason="Unimplemented")
189def test_get_ppa():
190 # TODO: In fake_config, set ppa_name, team_name, and lp
191 # ppa = script.get_ppa(fake_config)
192 # TODO: Verify type(ppa) is Ppa
193 # TODO: Verify that ppa_name is set properly in ppa
194 # TODO: Verify that team_name is set properly in ppa
195 # TODO: Verify that lp is set properly in ppa
196 pass
197
198
199@pytest.mark.xfail(reason="Unimplemented")187@pytest.mark.xfail(reason="Unimplemented")
200def test_command_create(fake_config):188def test_command_create(fake_config):
201 assert script.command_create(fake_config) == 0189 assert script.command_create(fake_config) == 0
diff --git a/tests/test_subtest.py b/tests/test_subtest.py
index 04a2dc1..70f37c5 100644
--- a/tests/test_subtest.py
+++ b/tests/test_subtest.py
@@ -16,7 +16,7 @@ import sys
16import pytest16import pytest
1717
18sys.path.insert(0, os.path.realpath(18sys.path.insert(0, os.path.realpath(
19 os.path.join(os.path.dirname(__file__), "..")))19 os.path.join(os.path.dirname(os.path.realpath(__file__)), "..")))
2020
21from ppa.subtest import Subtest21from ppa.subtest import Subtest
2222
diff --git a/tests/test_trigger.py b/tests/test_trigger.py
index 87092c9..3d8541a 100644
--- a/tests/test_trigger.py
+++ b/tests/test_trigger.py
@@ -16,7 +16,7 @@ import sys
16import pytest16import pytest
1717
18sys.path.insert(0, os.path.realpath(18sys.path.insert(0, os.path.realpath(
19 os.path.join(os.path.dirname(__file__), "..")))19 os.path.join(os.path.dirname(os.path.realpath(__file__)), "..")))
2020
21from ppa.trigger import Trigger21from ppa.trigger import Trigger
2222
diff --git a/tests/test_version.py b/tests/test_version.py
index fa6066f..63690c3 100644
--- a/tests/test_version.py
+++ b/tests/test_version.py
@@ -10,22 +10,25 @@
1010
11import os11import os
12import sys12import sys
13
13sys.path.insert(0, os.path.realpath(14sys.path.insert(0, os.path.realpath(
14 os.path.join(os.path.dirname(__file__), "..")))15 os.path.join(os.path.dirname(os.path.realpath(__file__)), "..")))
1516
16from ppa._version import __version__, __version_info__17from ppa._version import __version__, __version_info__
1718
1819
19def test_version():20def test_version():
20 assert(type(__version__) is str)21 """Checks that the __version__ is specified correctly."""
21 assert('.' in __version__)22 assert type(__version__) is str
22 assert(__version__[0].isdigit())23 assert '.' in __version__
23 assert(__version__[-1] != '.')24 assert __version__[0].isdigit()
25 assert __version__[-1] != '.'
2426
2527
26def test_version_info():28def test_version_info():
27 assert(type(__version_info__) is tuple)29 """Checks that the __version_info__ is specified correctly."""
28 assert(len(__version_info__) > 1)30 assert type(__version_info__) is tuple
31 assert len(__version_info__) > 1
29 for elem in __version_info__:32 for elem in __version_info__:
30 assert(type(elem) is int)33 assert type(elem) is int
31 assert(elem >= 0)34 assert elem >= 0

Subscribers

People subscribed via source and target branches

to all changes: