Merge ppa-dev-tools:improve-ppa-address-handling into ppa-dev-tools:main
- Git
- lp:ppa-dev-tools
- improve-ppa-address-handling
- Merge into main
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) |
||||||||||||
Related bugs: |
|
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 |
Commit message
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.
- 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.
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:
222405e..e5b49d1 main -> main
Thank you again for the review, Lena!
Preview Diff
1 | diff --git a/README.md b/README.md |
2 | index 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 | ``` |
21 | diff --git a/ppa/ppa.py b/ppa/ppa.py |
22 | index 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) |
193 | diff --git a/ppa/ppa_group.py b/ppa/ppa_group.py |
194 | index 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 | |
264 | diff --git a/scripts/ppa b/scripts/ppa |
265 | index 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 | |
400 | diff --git a/tests/helpers.py b/tests/helpers.py |
401 | new file mode 100644 |
402 | index 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 |
496 | diff --git a/tests/test_io.py b/tests/test_io.py |
497 | index 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 | |
519 | diff --git a/tests/test_job.py b/tests/test_job.py |
520 | index 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" |
581 | diff --git a/tests/test_lp.py b/tests/test_lp.py |
582 | index 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 |
595 | diff --git a/tests/test_ppa.py b/tests/test_ppa.py |
596 | index 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' |
674 | diff --git a/tests/test_ppa_group.py b/tests/test_ppa_group.py |
675 | index 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 |
849 | diff --git a/tests/test_result.py b/tests/test_result.py |
850 | index 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 | |
862 | diff --git a/tests/test_scripts_ppa.py b/tests/test_scripts_ppa.py |
863 | index 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 |
894 | diff --git a/tests/test_subtest.py b/tests/test_subtest.py |
895 | index 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 | |
907 | diff --git a/tests/test_trigger.py b/tests/test_trigger.py |
908 | index 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 | |
920 | diff --git a/tests/test_version.py b/tests/test_version.py |
921 | index 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 |
LGTM, just some small code cleanup comments. All logic looks good