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
1diff --git a/README.md b/README.md
2index 4b5d34d..d26e1a4 100644
3--- a/README.md
4+++ b/README.md
5@@ -43,7 +43,7 @@ $ dput ppa:my-name/my-ppa some-package.changes
6 Wait until all packages in the PPA have finished building
7
8 ```
9-$ ppa wait my-ppa
10+$ ppa wait ppa:my-name/my-ppa
11 ```
12
13 Set the public description for a PPA from a file
14@@ -55,5 +55,5 @@ $ cat some-package/README | ppa desc ppa:my-name/my-ppa
15 Delete the PPA
16
17 ```
18-$ ppa destroy my-ppa
19+$ ppa destroy ppa:my-name/my-ppa
20 ```
21diff --git a/ppa/ppa.py b/ppa/ppa.py
22index 2617346..888df48 100755
23--- a/ppa/ppa.py
24+++ b/ppa/ppa.py
25@@ -8,6 +8,8 @@
26 # Released under GNU GPLv2 or later, read the file 'LICENSE.GPLv2+' for
27 # more information.
28
29+import re
30+
31 from textwrap import indent
32 from functools import lru_cache
33 from lazr.restfulclient.errors import BadRequest, NotFound
34@@ -17,7 +19,8 @@ class PpaDoesNotExist(BaseException):
35 """Exception indicating a requested PPA could not be found."""
36
37 def __init__(self, ppa_name, team_name, message=None):
38- """
39+ """Initializes the exception object.
40+
41 :param str ppa_name: The name of the missing PPA.
42 :param str message: An error message.
43 """
44@@ -36,40 +39,6 @@ class PpaDoesNotExist(BaseException):
45 return f"The PPA '{self.ppa_name}' does not exist for team or user '{self.team_name}'"
46
47
48-def ppa_address_split(ppa_address, default_team='me'):
49- """Parse an address for a PPA into its team and name components
50- """
51- if ppa_address.startswith('ppa:'):
52- if '/' not in ppa_address:
53- return (None, None)
54- rem = ppa_address.split('ppa:', 1)[1]
55- team_name = rem.split('/', 1)[0]
56- ppa_name = rem.split('/', 1)[1]
57- else:
58- team_name = default_team
59- ppa_name = ppa_address
60- return (team_name, ppa_name)
61-
62-
63-def get_das(distro, series_name, arch_name):
64- """Retrive the arch-series for the given distro.
65-
66- :param distribution distro: The Launchpad distribution object.
67- :param str series_name: The distro's codename for the series.
68- :param str arch_name: The hardware architecture.
69- :rtype: distro_arch_series
70- :returns: A Launchpad distro_arch_series object, or None on error.
71- """
72- if series_name is None or series_name == '':
73- return None
74-
75- for series in distro.series:
76- if series.name != series_name:
77- continue
78- return series.getDistroArchSeries(archtag=arch_name)
79- return None
80-
81-
82 class Ppa:
83 """Encapsulates data needed to access and conveniently wrap a PPA
84
85@@ -77,7 +46,7 @@ class Ppa:
86 of data from the remote.
87 """
88 def __init__(self, ppa_name, team_name, ppa_description=None, service=None):
89- """Initializes a new Ppa object for a given PPA
90+ """Initializes a new Ppa object for a given PPA.
91
92 This creates only the local representation of the PPA, it does
93 not cause a new PPA to be created in Launchpad. For that, see
94@@ -102,6 +71,11 @@ class Ppa:
95 self._service = service
96
97 def __repr__(self):
98+ """Machine-parsable unique representation of object.
99+
100+ :rtype: str
101+ :returns: Official string representation of the object.
102+ """
103 return (f'{self.__class__.__name__}('
104 f'ppa_name={self.ppa_name!r}, team_name={self.team_name!r})')
105
106@@ -130,7 +104,7 @@ class Ppa:
107 @property
108 @lru_cache
109 def address(self):
110- """The proper identifier of the PPA
111+ """The proper identifier of the PPA.
112
113 :rtype: str
114 :returns: The full identification string for the PPA.
115@@ -371,3 +345,77 @@ class Ppa:
116 if not retval:
117 print("Successfully published all builds for all architectures")
118 return retval
119+
120+
121+def ppa_address_split(ppa_address, default_team=None):
122+ """Parse an address for a PPA into its team and name components.
123+
124+ :param str ppa_address: A ppa name or address.
125+ :param str default_team: (Optional) name of team to use if missing.
126+ :rtype: tuple(str, str)
127+ :returns: The team name and ppa name as a tuple, or (None, None) on error.
128+ """
129+ if not ppa_address or len(ppa_address)<2:
130+ return (None, None)
131+ if ppa_address.startswith('ppa:'):
132+ if '/' not in ppa_address:
133+ return (None, None)
134+ rem = ppa_address.split('ppa:', 1)[1]
135+ team_name = rem.split('/', 1)[0]
136+ ppa_name = rem.split('/', 1)[1]
137+ elif ppa_address.startswith('http'):
138+ # Only launchpad PPA urls are supported
139+ m = re.search(r'https:\/\/launchpad\.net\/~([^/]+)\/\+archive\/ubuntu\/(.+)$', ppa_address)
140+ if not m:
141+ return (None, None)
142+ team_name = m.group(1)
143+ ppa_name = m.group(2)
144+ elif '/' in ppa_address:
145+ team_name = ppa_address.split('/', 1)[0]
146+ ppa_name = ppa_address.split('/', 1)[1]
147+ else:
148+ team_name = default_team
149+ ppa_name = ppa_address
150+
151+ if (team_name and ppa_name
152+ and not (any(x.isupper() for x in team_name))
153+ and not (any(x.isupper() for x in ppa_name))
154+ and ppa_name.isascii()
155+ and '/' not in ppa_name
156+ and len(ppa_name)>1):
157+ return (team_name, ppa_name)
158+
159+ return (None, None)
160+
161+
162+def get_das(distro, series_name, arch_name):
163+ """Retrive the arch-series for the given distro.
164+
165+ :param distribution distro: The Launchpad distribution object.
166+ :param str series_name: The distro's codename for the series.
167+ :param str arch_name: The hardware architecture.
168+ :rtype: distro_arch_series
169+ :returns: A Launchpad distro_arch_series object, or None on error.
170+ """
171+ if series_name is None or series_name == '':
172+ return None
173+
174+ for series in distro.series:
175+ if series.name != series_name:
176+ continue
177+ return series.getDistroArchSeries(archtag=arch_name)
178+ return None
179+
180+
181+def get_ppa(lp, config):
182+ """Load the specified PPA from Launchpad
183+
184+ :param Lp lp: The Launchpad wrapper object.
185+ :param dict config: Configuration param:value map.
186+ :rtype: Ppa
187+ :returns: Specified PPA as a Ppa object.
188+ """
189+ return Ppa(
190+ ppa_name=config.get('ppa_name', None),
191+ team_name=config.get('team_name', None),
192+ service=lp)
193diff --git a/ppa/ppa_group.py b/ppa/ppa_group.py
194index 1a9c8b7..0160574 100755
195--- a/ppa/ppa_group.py
196+++ b/ppa/ppa_group.py
197@@ -16,10 +16,11 @@ from lazr.restfulclient.errors import BadRequest
198
199
200 class PpaAlreadyExists(BaseException):
201- '''Exception indicating a PPA operation could not be performed'''
202+ """Exception indicating a PPA operation could not be performed."""
203
204 def __init__(self, ppa_name, message=None):
205- """
206+ """Initializes the exception object.
207+
208 :param str ppa_name: The name of the pre-existing PPA.
209 :param str message: An error message.
210 """
211@@ -44,25 +45,31 @@ class PpaGroup:
212 This class provides a proxy object for interacting with collections
213 of PPA.
214 """
215- def __init__(self, service, name='me'):
216- """
217+ def __init__(self, service, name):
218+ """Initializes a new PpaGroup object for a named person or team.
219+
220 :param launchpadlib.service service: The Launchpad service object.
221- :param str name: Launchpad username
222+ :param str name: Launchpad team or user name
223 """
224- assert(service is not None)
225+ assert service is not None
226 self.service = service
227-
228- if name == 'me':
229- me = self.service.me
230- self.name = me.name
231- else:
232- self.name = name
233+ self.name = name
234
235 def __repr__(self):
236+ """Machine-parsable unique representation of object.
237+
238+ :rtype: str
239+ :returns: Official string representation of the object.
240+ """
241 return (f'{self.__class__.__name__}('
242 f'service={self.service!r}, name={self.name!r})')
243
244 def __str__(self):
245+ """Human-readable summary of the object.
246+
247+ :rtype: str
248+ :returns: Printable summary of the object.
249+ """
250 return 'tbd'
251
252 @property
253@@ -70,8 +77,8 @@ class PpaGroup:
254 def team(self):
255 """The team that owns this collection of PPAs.
256
257- :rtype: tbd
258- :returns: tbd
259+ :rtype: launchpadlib.person
260+ :returns: Launchpad person object that owns this PPA.
261 """
262 return self.service.people[self.name]
263
264diff --git a/scripts/ppa b/scripts/ppa
265index 361d91b..6965d44 100755
266--- a/scripts/ppa
267+++ b/scripts/ppa
268@@ -77,7 +77,12 @@ from ppa.job import (
269 show_running
270 )
271 from ppa.lp import Lp
272-from ppa.ppa import Ppa, PpaDoesNotExist
273+from ppa.ppa import (
274+ get_ppa,
275+ ppa_address_split,
276+ Ppa,
277+ PpaDoesNotExist
278+)
279 from ppa.ppa_group import PpaGroup, PpaAlreadyExists
280 from ppa.result import (
281 Result,
282@@ -87,7 +92,7 @@ from ppa.text import o2str
283 from ppa.trigger import Trigger
284
285 import ppa.debug
286-from ppa.debug import dbg, die, warn
287+from ppa.debug import dbg, warn
288
289
290 def UNIMPLEMENTED():
291@@ -197,7 +202,7 @@ def create_arg_parser():
292 return parser
293
294
295-def create_config(args):
296+def create_config(lp, args):
297 """Creates config object by loading from file and adding args.
298
299 This routine merges the command line parameter values with data
300@@ -208,6 +213,7 @@ def create_config(args):
301 This permits setting static values in the config file(s), and using
302 the command line args for variable settings and overrides.
303
304+ :param launchpadlib.service lp: The Launchpad service object.
305 :param Namespace args: The parsed args from ArgumentParser.
306 :rtype: dict
307 :returns: dict of configuration parameters and values, or None on error
308@@ -233,21 +239,12 @@ def create_config(args):
309 for k, v in DEFAULT_CONFIG.items():
310 config.setdefault(k, v)
311
312- # TODO: function to convert string to ppa_name, team_name
313- if args.ppa_name.startswith('ppa:'):
314- if '/' not in args.ppa_name:
315- die("Invalid ppa name '{}'".format(args.ppa_name))
316- rem = args.ppa_name.split('ppa:', 1)[1]
317- config['team_name'] = rem.split('/', 1)[0]
318- config['ppa_name'] = rem.split('/', 1)[1]
319- elif '/' in args.ppa_name:
320- config['team_name'] = args.ppa_name.split('/', 1)[0]
321- config['ppa_name'] = args.ppa_name.split('/', 1)[1]
322- else:
323- config['ppa_name'] = args.ppa_name
324- # TODO: Set team_name to current lp_username if available
325- if 'ppa_name' not in config or 'team_name' not in config:
326- warn("Unknown ppa or team name")
327+ lp_username = None
328+ if lp.me:
329+ lp_username = lp.me.name
330+ config['team_name'], config['ppa_name'] = ppa_address_split(args.ppa_name, lp_username)
331+ if not config['team_name'] or not config['ppa_name']:
332+ warn("Invalid ppa name '{}'".format(args.ppa_name))
333 return None
334
335 if args.dry_run:
336@@ -263,20 +260,6 @@ def create_config(args):
337 return config
338
339
340-def get_ppa(lp, config):
341- """Load the specified PPA from Launchpad
342-
343- :param Lp lp: The Launchpad wrapper object.
344- :param dict config: Configuration param:value map.
345- :rtype: Ppa
346- :returns: Specified PPA as a Ppa object.
347- """
348- return Ppa(
349- ppa_name=config.get('ppa_name', None),
350- team_name=config.get('team_name', None),
351- service=lp)
352-
353-
354 ################
355 ### Commands ###
356 ################
357@@ -296,12 +279,13 @@ def command_create(lp, config):
358
359 ppa_name = config.get('ppa_name')
360 if not ppa_name:
361- warn("Could not determine ppa_name")
362+ warn("Could not determine PPA name")
363 return os.EX_USAGE
364
365 team_name = config.get('team_name')
366 if not team_name:
367- team_name = 'me'
368+ warn("Could not determine team name")
369+ return os.EX_USAGE
370
371 architectures = config.get('architectures', ARCHES_PPA)
372
373@@ -404,8 +388,13 @@ def command_list(lp, config, filter_func=None):
374 if not lp:
375 return 1
376
377+ team_name = config.get('team_name')
378+ if not team_name:
379+ warn("Could not determine team name")
380+ return os.EX_USAGE
381+
382 try:
383- ppa_group = PpaGroup(service=lp)
384+ ppa_group = PpaGroup(service=lp, name=team_name)
385 for ppa in ppa_group.ppas:
386 print(ppa.address)
387 return os.EX_OK
388@@ -629,8 +618,9 @@ def main(args):
389 """
390 config = create_config(args)
391 if not config:
392- parser.error("Invalid parameters")
393- return 1
394+ return os.EX_CONFIG
395+
396+ ppa.debug.DEBUGGING = config.get('debug', False)
397 dbg("Configuration:")
398 dbg(config)
399
400diff --git a/tests/helpers.py b/tests/helpers.py
401new file mode 100644
402index 0000000..c774cfc
403--- /dev/null
404+++ b/tests/helpers.py
405@@ -0,0 +1,90 @@
406+#!/usr/bin/env python3
407+# -*- coding: utf-8 -*-
408+
409+# Author: Bryce Harrington <bryce@canonical.com>
410+#
411+# Copyright (C) 2019 Bryce W. Harrington
412+#
413+# Released under GNU GPLv2 or later, read the file 'LICENSE.GPLv2+' for
414+# more information.
415+
416+import os
417+import sys
418+
419+sys.path.insert(0, os.path.realpath(
420+ os.path.join(os.path.dirname(os.path.realpath(__file__)), "..")))
421+
422+from ppa.ppa import Ppa
423+from ppa.ppa_group import PpaGroup, PpaAlreadyExists
424+
425+
426+class PersonMock:
427+ """A stand-in for a Launchpad Person object."""
428+ def __init__(self, name):
429+ self.name = name
430+ self._ppas = []
431+
432+ def createPPA(self, name, description, displayname):
433+ for ppa in self._ppas:
434+ if ppa.name == name:
435+ raise PpaAlreadyExists(name)
436+ new_ppa = Ppa(name, self.name, description)
437+ self._ppas.append(new_ppa)
438+ return True
439+
440+ def lp_save(self):
441+ return True
442+
443+ @property
444+ def ppas(self):
445+ return self._ppas
446+
447+
448+class LaunchpadMock:
449+ """A stand-in for Launchpad."""
450+ def __init__(self):
451+ self.people = { 'me': PersonMock('me') }
452+
453+ def add_person(self, name):
454+ print("Adding person %s" %(name))
455+ self.people[name] = PersonMock(name)
456+
457+ @property
458+ def me(self):
459+ return self.people['me']
460+
461+
462+class LpServiceMock:
463+ """A stand-in for the Lp service object."""
464+ def __init__(self):
465+ self.launchpad = LaunchpadMock()
466+
467+ @property
468+ def me(self):
469+ return self.launchpad.people['me']
470+
471+ @property
472+ def people(self):
473+ return self.launchpad.people
474+
475+ def get_bug(self, bug_id):
476+ class BugMock:
477+ @property
478+ def title(self):
479+ return "Mock bug report"
480+
481+ @property
482+ def description(self):
483+ return "Description line 1\n\ndescription line 2"
484+
485+ return BugMock()
486+
487+
488+class RequestResponseMock:
489+ """A stand-in for a request result."""
490+ def __init__(self, text):
491+ self._text = text.encode('utf-8')
492+
493+ def read(self):
494+ """Simply returns the exact text provided in initializer."""
495+ return self._text
496diff --git a/tests/test_io.py b/tests/test_io.py
497index 57f8737..c15d2af 100644
498--- a/tests/test_io.py
499+++ b/tests/test_io.py
500@@ -15,13 +15,16 @@ import sys
501 import urllib
502
503 sys.path.insert(0, os.path.realpath(
504- os.path.join(os.path.dirname(__file__), "..")))
505+ os.path.join(os.path.dirname(os.path.realpath(__file__)), "..")))
506
507 from ppa.io import open_url
508
509
510 def test_open_url(tmp_path):
511- """Checks that the open_url() object reads from a valid URL."""
512+ """Checks that the open_url() object reads from a valid URL.
513+
514+ :param fixture tmp_path: Temp dir.
515+ """
516 f = tmp_path / "open_url.txt"
517 f.write_text("abcde")
518
519diff --git a/tests/test_job.py b/tests/test_job.py
520index 44491c2..78a12c7 100644
521--- a/tests/test_job.py
522+++ b/tests/test_job.py
523@@ -14,19 +14,10 @@ import os
524 import sys
525
526 sys.path.insert(0, os.path.realpath(
527- os.path.join(os.path.dirname(__file__), "..")))
528+ os.path.join(os.path.dirname(os.path.realpath(__file__)), "..")))
529
530 from ppa.job import Job, get_running, get_waiting
531-
532-
533-class ResponseMock:
534- """Synthetic response object"""
535- def __init__(self, text):
536- self._text = text.encode('utf-8')
537-
538- def read(self):
539- """Simply returns the exact text provided in initializer."""
540- return self._text
541+from tests.helpers import RequestResponseMock
542
543
544 def test_object():
545@@ -81,7 +72,7 @@ def test_request_url():
546
547
548 def test_get_running():
549- """Checks output from the get_running() command"""
550+ """Checks output from the get_running() command."""
551 json_text = ('{"mypackage": {"my-job-id": {"focal": { "arm64": ['
552 '{"submit-time": "2022-08-19 20:59:01", '
553 '"triggers": ["yourpackage/1.2.3"], '
554@@ -89,7 +80,7 @@ def test_get_running():
555 '1234, '
556 '"Log Output Here"'
557 '] } } } }')
558- fake_response = ResponseMock(json_text)
559+ fake_response = RequestResponseMock(json_text)
560 job = next(get_running(fake_response, series='focal', ppa='ppa:me/myppa'))
561 assert repr(job) == "Job(source_package='mypackage', series='focal', arch='arm64')"
562 assert job.triggers == ["yourpackage/1.2.3"]
563@@ -97,7 +88,7 @@ def test_get_running():
564
565
566 def test_get_waiting():
567- """Checks output from the get_waiting() command"""
568+ """Checks output from the get_waiting() command."""
569 # TODO: I think ppas need to be in "ppa" instead of under "ubuntu" but need to doublecheck.
570 json_text = ('{ "ubuntu": { "focal": { "amd64": ['
571 ' "a\\n{\\"requester\\": \\"you\\",'
572@@ -108,7 +99,7 @@ def test_get_waiting():
573 ' \\"ppas\\": [ \\"ppa:me/myppa\\" ],'
574 ' \\"triggers\\": [ \\"c/3.2-1\\", \\"d/2-2\\" ] }"'
575 '] } } }')
576- fake_response = ResponseMock(json_text)
577+ fake_response = RequestResponseMock(json_text)
578 job = next(get_waiting(fake_response, series='focal', ppa='ppa:me/myppa'))
579 assert job
580 assert job.source_package == "b"
581diff --git a/tests/test_lp.py b/tests/test_lp.py
582index d5978ce..98f4888 100644
583--- a/tests/test_lp.py
584+++ b/tests/test_lp.py
585@@ -34,7 +34,8 @@ from mock import Mock
586 import pytest
587 import logging
588
589-sys.path.insert(0, os.path.realpath(os.path.join(os.path.dirname(__file__), "..")))
590+sys.path.insert(0, os.path.realpath(
591+ os.path.join(os.path.dirname(os.path.realpath(__file__)), "..")))
592
593 from ppa.lp import Lp
594 from launchpadlib.launchpad import Launchpad
595diff --git a/tests/test_ppa.py b/tests/test_ppa.py
596index a71336f..2410f83 100644
597--- a/tests/test_ppa.py
598+++ b/tests/test_ppa.py
599@@ -11,26 +11,69 @@
600 import os
601 import sys
602
603+import pytest
604+
605 sys.path.insert(0, os.path.realpath(
606- os.path.join(os.path.dirname(__file__), "..")))
607+ os.path.join(os.path.dirname(os.path.realpath(__file__)), "..")))
608
609-from ppa.ppa import Ppa
610+from ppa.ppa import Ppa, ppa_address_split, get_ppa
611
612
613 def test_object():
614- """Check that PPA objects can be instantiated"""
615+ """Check that PPA objects can be instantiated."""
616 ppa = Ppa('test-ppa-name', 'test-team-name')
617 assert ppa
618
619
620 def test_description():
621- """Check specifying a description when creating a PPA"""
622+ """Check specifying a description when creating a PPA."""
623 ppa = Ppa('test-ppa-name', 'test-team-name', 'test-description')
624
625 assert 'test-description' in ppa.ppa_description
626
627
628 def test_address():
629- """Check getting the PPA address"""
630+ """Check getting the PPA address."""
631 ppa = Ppa('test', 'team')
632 assert ppa.address == "ppa:team/test"
633+
634+
635+@pytest.mark.parametrize('address, default_team, expected', [
636+ # Successful cases
637+ ('bb', 'me', ('me', 'bb')),
638+ ('123', 'me', ('me', '123')),
639+ ('a/123', 'me', ('a', '123')),
640+ ('ppa:a/bb', None, ('a', 'bb')),
641+ ('ppa:ç/bb', None, ('ç', 'bb')),
642+ ('https://launchpad.net/~a/+archive/ubuntu/bb', None, ('a', 'bb')),
643+
644+ # Expected failure cases
645+ ('ppa:', None, (None, None)),
646+ (None, None, (None, None)),
647+ ('', None, (None, None)),
648+ ('/', None, (None, None)),
649+ (':/', None, (None, None)),
650+ ('////', None, (None, None)),
651+ ('ppa:/', None, (None, None)),
652+ ('ppa:a/', None, (None, None)),
653+ ('ppa:/bb', None, (None, None)),
654+ ('ppa:a/bç', None, (None, None)),
655+ ('ppa:A/bb', None, (None, None)),
656+ ('ppa/a/bb', None, (None, None)),
657+ ('ppa:a/bb/c', None, (None, None)),
658+ ('ppa:a/bB', None, (None, None)),
659+ ('http://launchpad.net/~a/+archive/ubuntu/bb', None, (None, None)),
660+ ('https://example.com/~a/+archive/ubuntu/bb', None, (None, None)),
661+ ('https://launchpad.net/~a/+archive/nobuntu/bb', None, (None, None)),
662+])
663+def test_ppa_address_split(address, default_team, expected):
664+ """Check ppa address input strings can be parsed properly."""
665+ result = ppa_address_split(address, default_team=default_team)
666+ assert result == expected
667+
668+
669+def test_get_ppa():
670+ ppa = get_ppa(None, {'team_name': 'a', 'ppa_name': 'bb'})
671+ assert type(ppa) is Ppa
672+ assert ppa.team_name == 'a'
673+ assert ppa.ppa_name == 'bb'
674diff --git a/tests/test_ppa_group.py b/tests/test_ppa_group.py
675index e03a841..63907fd 100644
676--- a/tests/test_ppa_group.py
677+++ b/tests/test_ppa_group.py
678@@ -14,141 +14,83 @@ import sys
679 import pytest
680
681 sys.path.insert(0, os.path.realpath(
682- os.path.join(os.path.dirname(__file__), "..")))
683+ os.path.join(os.path.dirname(os.path.realpath(__file__)), "..")))
684
685 from ppa.ppa import Ppa
686 from ppa.ppa_group import PpaGroup, PpaAlreadyExists
687-
688-
689-class PersonMock:
690- def __init__(self, name):
691- self.name = name
692- self._ppas = []
693-
694- def createPPA(self, name, description, displayname):
695- for ppa in self._ppas:
696- if ppa.name == name:
697- raise PpaAlreadyExists(name)
698- new_ppa = Ppa(name, self.name, description)
699- self._ppas.append(new_ppa)
700- return True
701-
702- def lp_save(self):
703- return True
704-
705- @property
706- def ppas(self):
707- return self._ppas
708-
709-
710-class LaunchpadMock:
711- def __init__(self):
712- self.people = {'me': PersonMock('me')}
713-
714- def add_person(self, name):
715- print(f"Adding person {name}")
716- self.people[name] = PersonMock(name)
717-
718- @property
719- def me(self):
720- return self.people['me']
721-
722-
723-class LpServiceMock:
724- def __init__(self):
725- self.launchpad = LaunchpadMock()
726-
727- @property
728- def me(self):
729- return self.launchpad.people['me']
730-
731- @property
732- def people(self):
733- return self.launchpad.people
734-
735- def get_bug(self, bug_id):
736- class BugMock:
737- @property
738- def title(self):
739- return "Mock bug report"
740-
741- @property
742- def description(self):
743- return "Description line 1\n\ndescription line 2"
744-
745- return BugMock()
746+from tests.helpers import PersonMock, LaunchpadMock, LpServiceMock
747
748
749 def test_object():
750 """Checks that PpaGroup objects can be instantiated."""
751- ppa_group = PpaGroup(service=LpServiceMock())
752- assert(ppa_group)
753+ ppa_group = PpaGroup(service=LpServiceMock(), name=None)
754+ assert ppa_group
755
756
757 def test_create_ppa():
758- """Checks that PpaGroups can create PPAs"""
759+ """Checks that PpaGroups can create PPAs."""
760 name = 'test_ppa'
761- ppa_group = PpaGroup(service=LpServiceMock())
762+ ppa_group = PpaGroup(service=LpServiceMock(), name='me')
763 ppa = ppa_group.create(name)
764- assert(ppa is not None)
765- assert(name in ppa.address)
766- assert(type(ppa.description) is str)
767+ assert ppa is not None
768+ assert name in ppa.address
769+ assert type(ppa.description) is str
770
771
772 def test_create_existing_ppa():
773- """Check exception creating an already created PPA"""
774+ """Check exception creating an already created PPA."""
775 name = 'test_ppa'
776- ppa_group = PpaGroup(service=LpServiceMock())
777+ ppa_group = PpaGroup(service=LpServiceMock(), name='me')
778 ppa_group.create(name)
779 with pytest.raises(PpaAlreadyExists):
780 ppa_group.create(name)
781
782
783 def test_create_with_description():
784- """Check setting a description for a PPA"""
785- ppa_group = PpaGroup(service=LpServiceMock())
786+ """Check setting a description for a PPA."""
787+ ppa_group = PpaGroup(service=LpServiceMock(), name='me')
788 description = 'PPA Test Description'
789 ppa = ppa_group.create('test_ppa_with_description', description)
790- assert(ppa is not None)
791- assert(ppa.description == description)
792+ assert ppa is not None
793+ assert ppa.description == description
794
795
796 def test_create_with_team():
797- """Check creating a PPA for a particular team"""
798+ """Check creating a PPA for a particular team."""
799 lp = LpServiceMock()
800 lp.launchpad.add_person('test_team_name')
801 ppa_group = PpaGroup(service=lp, name='test_team_name')
802 ppa = ppa_group.create('ppa_test_name')
803- assert(ppa is not None)
804- assert(ppa.address == 'ppa:test_team_name/ppa_test_name')
805+ assert ppa is not None
806+ assert ppa.address == 'ppa:test_team_name/ppa_test_name'
807
808
809 def test_create_for_lpbug():
810- """Check associating a bug # when creating a PPA"""
811- ppa_group = PpaGroup(service=LpServiceMock())
812+ """Check associating a bug # when creating a PPA."""
813+ ppa_group = PpaGroup(service=LpServiceMock(), name='me')
814 lpbug = '1234567'
815 ppa = ppa_group.create('lp' + lpbug)
816- assert(ppa is not None)
817- assert(lpbug in ppa.description)
818+ assert ppa is not None
819+ assert lpbug in ppa.description
820
821
822 def test_create_for_merge_proposal():
823- """Check associating a merge proposal when creating a PPA"""
824- ppa_group = PpaGroup(service=LpServiceMock())
825+ """Check associating a merge proposal when creating a PPA."""
826+ ppa_group = PpaGroup(service=LpServiceMock(), name='me')
827 version = '1.2.3-4'
828 ppa = ppa_group.create('merge.' + version)
829- assert(ppa is not None)
830- assert(version in ppa.description)
831+ assert ppa is not None
832+ assert version in ppa.description
833
834
835 def test_list_ppas():
836- """Check listing the PPAs for a PPA group"""
837+ """Check listing the PPAs for a PPA group."""
838 test_ppa_list = ['a', 'b', 'c', 'd']
839- ppa_group = PpaGroup(service=LpServiceMock())
840+ ppa_group = PpaGroup(service=LpServiceMock(), name='me')
841
842 # Add several ppas
843 for ppa in test_ppa_list:
844 ppa_group.create(ppa)
845
846 ppas = [ppa.name for ppa in list(ppa_group.ppas)]
847- assert(test_ppa_list == ppas)
848+ assert test_ppa_list == ppas
849diff --git a/tests/test_result.py b/tests/test_result.py
850index cbd574c..8c498a4 100644
851--- a/tests/test_result.py
852+++ b/tests/test_result.py
853@@ -17,7 +17,7 @@ import time
854 import gzip
855
856 sys.path.insert(0, os.path.realpath(
857- os.path.join(os.path.dirname(__file__), "..")))
858+ os.path.join(os.path.dirname(os.path.realpath(__file__)), "..")))
859 DATA_DIR = os.path.realpath(
860 os.path.join(os.path.dirname(__file__), "data"))
861
862diff --git a/tests/test_scripts_ppa.py b/tests/test_scripts_ppa.py
863index fed190b..7a578f7 100644
864--- a/tests/test_scripts_ppa.py
865+++ b/tests/test_scripts_ppa.py
866@@ -19,7 +19,7 @@ import argparse
867 import pytest
868
869 SCRIPT_NAME = 'ppa'
870-BASE_PATH = os.path.realpath(os.path.join(os.path.dirname(__file__), '..'))
871+BASE_PATH = os.path.realpath(os.path.join(os.path.dirname(os.path.realpath(__file__)), '..'))
872 sys.path.insert(0, BASE_PATH)
873
874 if '.pybuild' in BASE_PATH:
875@@ -184,18 +184,6 @@ def test_create_config():
876 pass
877
878
879-# TODO: Monkeypatch in Lp = MockLp
880-@pytest.mark.xfail(reason="Unimplemented")
881-def test_get_ppa():
882- # TODO: In fake_config, set ppa_name, team_name, and lp
883- # ppa = script.get_ppa(fake_config)
884- # TODO: Verify type(ppa) is Ppa
885- # TODO: Verify that ppa_name is set properly in ppa
886- # TODO: Verify that team_name is set properly in ppa
887- # TODO: Verify that lp is set properly in ppa
888- pass
889-
890-
891 @pytest.mark.xfail(reason="Unimplemented")
892 def test_command_create(fake_config):
893 assert script.command_create(fake_config) == 0
894diff --git a/tests/test_subtest.py b/tests/test_subtest.py
895index 04a2dc1..70f37c5 100644
896--- a/tests/test_subtest.py
897+++ b/tests/test_subtest.py
898@@ -16,7 +16,7 @@ import sys
899 import pytest
900
901 sys.path.insert(0, os.path.realpath(
902- os.path.join(os.path.dirname(__file__), "..")))
903+ os.path.join(os.path.dirname(os.path.realpath(__file__)), "..")))
904
905 from ppa.subtest import Subtest
906
907diff --git a/tests/test_trigger.py b/tests/test_trigger.py
908index 87092c9..3d8541a 100644
909--- a/tests/test_trigger.py
910+++ b/tests/test_trigger.py
911@@ -16,7 +16,7 @@ import sys
912 import pytest
913
914 sys.path.insert(0, os.path.realpath(
915- os.path.join(os.path.dirname(__file__), "..")))
916+ os.path.join(os.path.dirname(os.path.realpath(__file__)), "..")))
917
918 from ppa.trigger import Trigger
919
920diff --git a/tests/test_version.py b/tests/test_version.py
921index fa6066f..63690c3 100644
922--- a/tests/test_version.py
923+++ b/tests/test_version.py
924@@ -10,22 +10,25 @@
925
926 import os
927 import sys
928+
929 sys.path.insert(0, os.path.realpath(
930- os.path.join(os.path.dirname(__file__), "..")))
931+ os.path.join(os.path.dirname(os.path.realpath(__file__)), "..")))
932
933 from ppa._version import __version__, __version_info__
934
935
936 def test_version():
937- assert(type(__version__) is str)
938- assert('.' in __version__)
939- assert(__version__[0].isdigit())
940- assert(__version__[-1] != '.')
941+ """Checks that the __version__ is specified correctly."""
942+ assert type(__version__) is str
943+ assert '.' in __version__
944+ assert __version__[0].isdigit()
945+ assert __version__[-1] != '.'
946
947
948 def test_version_info():
949- assert(type(__version_info__) is tuple)
950- assert(len(__version_info__) > 1)
951+ """Checks that the __version_info__ is specified correctly."""
952+ assert type(__version_info__) is tuple
953+ assert len(__version_info__) > 1
954 for elem in __version_info__:
955- assert(type(elem) is int)
956- assert(elem >= 0)
957+ assert type(elem) is int
958+ assert elem >= 0

Subscribers

People subscribed via source and target branches

to all changes: