Merge ppa-dev-tools:tests-dashboard into ppa-dev-tools:main
- Git
- lp:ppa-dev-tools
- tests-dashboard
- Merge into main
Status: | Merged |
---|---|
Approved by: | Bryce Harrington |
Approved revision: | 81e5433dcb506903f321d6d0fb99a681f11991ba |
Merge reported by: | Bryce Harrington |
Merged at revision: | 81e5433dcb506903f321d6d0fb99a681f11991ba |
Proposed branch: | ppa-dev-tools:tests-dashboard |
Merge into: | ppa-dev-tools:main |
Diff against target: |
963 lines (+363/-120) 16 files modified
NEWS.md (+7/-0) README.md (+24/-0) ppa/debug.py (+14/-8) ppa/job.py (+20/-20) ppa/ppa.py (+16/-3) ppa/ppa_group.py (+15/-7) ppa/result.py (+11/-6) ppa/subtest.py (+7/-4) ppa/text.py (+14/-29) ppa/trigger.py (+5/-3) scripts/ppa (+92/-34) tests/test_job.py (+2/-2) tests/test_ppa_group.py (+6/-1) tests/test_result.py (+1/-1) tests/test_scripts_ppa.py (+75/-2) tests/test_text.py (+54/-0) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Athos Ribeiro (community) | Approve | ||
Ubuntu Server | Pending | ||
Canonical Server Reporter | Pending | ||
Review via email: mp+432123@code.launchpad.net |
Commit message
Description of the change
I am still running a version of terminator that does not support the
ansi hyperlink functionality, and for that reason was planning on
implementing something to test the terminal for support, and make
`ppa tests` automatically format links.
However, in researching into it, I see that once I upgrade to jammy's
version of Terminator I'll have the support, and a lot of other terminal
programs also already include it now. Furthermore, I've failed to find
if there's a provided way to check for support, other than just looking
up the terminal program's version number against a known-good list.
So rather than bother with all that logic (that'll become less and less
necessary over time), I'll just follow lp-test-ppa's solution of relying
on commandline params / config values for users to manually select what
they want, and leave it at that.
Anyway, by and large this is derivative of lp-test-ppa's output
formatting, but I've added some refinements. First, I've structured
the bulleting to follow the style we use in changelog entries (I like it).
I've attempted to condense the amount of output by eliminating redundant
text, re-ordering how things are displayed, and filtering out stuff I
think users won't care about. Likely, this will all require further
refinement, but the ultimate goal is to provide a convenient
dashboard-like view.
Beyond that, this also includes a variety of random fixes and cleanups
discovered during the preparation of this branch. Hopefully nothing
that causes too much distraction. Most notable of these is a fix for
the rather rare situation we're in where the old devel release is no
longer under development, but the next development release hasn't
opened.
- 81e5433... by Bryce Harrington
-
tests: Implement the --architectures option for test result display
Bryce Harrington (bryce) wrote : | # |
Noted on the refactoring change. I often squash things together in hopes it makes stuff easier to review, so it's good to know when it doesn't.
With bfd3865, it wasn't just a parameter name change, the type also changed from a string to a list. Normally I would have just pluralized series but since it's the same word singular or plural that would just be confusing, thus the name change. In hindsight, probably better to rename series->release and then to releases.
Thanks again!
Preview Diff
1 | diff --git a/NEWS.md b/NEWS.md |
2 | index d134499..533c92f 100644 |
3 | --- a/NEWS.md |
4 | +++ b/NEWS.md |
5 | @@ -1,3 +1,10 @@ |
6 | +# Unreleased # |
7 | + |
8 | +Autopkgtest action URLs are printed for packages in the PPA when running |
9 | +the `ppa tests` command. These can be loaded in a web browser by |
10 | +someone with core-dev permissions to start the test runs. `ppa tests` |
11 | +can then be re-run to check on the tests status and results. |
12 | + |
13 | # 0.2.1 Release # |
14 | |
15 | This corrects some packaging issues when generating .deb and .snap |
16 | diff --git a/README.md b/README.md |
17 | index d26e1a4..99f10ee 100644 |
18 | --- a/README.md |
19 | +++ b/README.md |
20 | @@ -57,3 +57,27 @@ Delete the PPA |
21 | ``` |
22 | $ ppa destroy ppa:my-name/my-ppa |
23 | ``` |
24 | + |
25 | +Auto-linked Autopkgtest Trigger URLS |
26 | +------------------------------------ |
27 | + |
28 | +By default, `ppa tests` will display autopkgtest triggers as hyperlinked |
29 | +text. The hyperlinking feature is supported on many newer terminal |
30 | +programs but as it's a relatively recent VTE function it may not yet be |
31 | +available in the terminal you use. The following terminal programs are |
32 | +believed to support it: |
33 | + |
34 | + - iTerm2 3.1 |
35 | + - DomTerm 1.0.2 |
36 | + - hTerm 1.76 |
37 | + - Terminology 1.3? |
38 | + - Gnome Terminal 3.26 (VTE 0.50) |
39 | + - Guake 3.2.1 (VTE 0.50) |
40 | + - TOXTerm 3.5.1 (VTE 0.50) |
41 | + - Tilix 3.26 (VTE 0.50) |
42 | + - Terminator 2.0 |
43 | + |
44 | +This is not a comprehensive list, and likely will lengthen swiftly. |
45 | +Meanwhile, if you have a non-supporting browser, the --show-urls option |
46 | +can be passed to `ppa tests` to make it display the raw URLs that can be |
47 | +manually cut and pasted into your web browser. |
48 | diff --git a/ppa/debug.py b/ppa/debug.py |
49 | index 009ead4..e6fac49 100644 |
50 | --- a/ppa/debug.py |
51 | +++ b/ppa/debug.py |
52 | @@ -10,25 +10,31 @@ |
53 | |
54 | import sys |
55 | import pprint |
56 | +import textwrap |
57 | |
58 | DEBUGGING = False |
59 | |
60 | |
61 | -def dbg(msg): |
62 | +def dbg(msg, wrap=0, prefix=None, indent=''): |
63 | """Prints information if debugging is enabled""" |
64 | if DEBUGGING: |
65 | if type(msg) is str: |
66 | - sys.stderr.write("{}\n".format(msg)) |
67 | + if wrap == 0 and indent != '': |
68 | + wrap = 72 |
69 | + if wrap > 0: |
70 | + if prefix is None and len(indent)>0: |
71 | + prefix = indent |
72 | + msg = textwrap.fill(msg, width=wrap, initial_indent=prefix, subsequent_indent=indent) |
73 | + sys.stderr.write(f"{msg}\n") |
74 | else: |
75 | pprint.pprint(msg) |
76 | |
77 | |
78 | def warn(msg): |
79 | - """Prints message to stderr""" |
80 | - sys.stderr.write("Warning: {}\n".format(msg)) |
81 | + """Prints warning message to stderr.""" |
82 | + sys.stderr.write(f"Warning: {msg}\n") |
83 | |
84 | |
85 | -def die(msg, code=1): |
86 | - """Prints message to stderr and exits with given code""" |
87 | - sys.stderr.write("Error: {}\n".format(msg)) |
88 | - sys.exit(code) |
89 | +def error(msg): |
90 | + """Prints error message to stderr.""" |
91 | + sys.stderr.write(f"Error: {msg}\n") |
92 | diff --git a/ppa/job.py b/ppa/job.py |
93 | index 91f015c..00dd92b 100755 |
94 | --- a/ppa/job.py |
95 | +++ b/ppa/job.py |
96 | @@ -78,7 +78,7 @@ class Job: |
97 | return f"{URL_AUTOPKGTEST}/request.cgi?{rel_str}{arch_str}{pkg_str}{trigger_str}" |
98 | |
99 | |
100 | -def get_running(response, series=None, ppa=None) -> Iterator[Job]: |
101 | +def get_running(response, releases=None, ppa=None) -> Iterator[Job]: |
102 | """Returns iterator currently running autopkgtests for given criteria |
103 | |
104 | Filters the list of running autopkgtest jobs by the given series |
105 | @@ -88,12 +88,12 @@ def get_running(response, series=None, ppa=None) -> Iterator[Job]: |
106 | results for that series or ppa. |
107 | |
108 | :param HTTPResponse response: Context manager; the response from urlopen() |
109 | - :param str series: The Ubuntu release codename criteria, or None. |
110 | + :param list[str] releases: The Ubuntu series codename(s), or None. |
111 | :param str ppa: The PPA address criteria, or None. |
112 | :rtype: Iterator[Job] |
113 | :returns: Currently running jobs, if any, or an empty list on error |
114 | """ |
115 | - for pkg, jobs in json.loads(response.read().decode('utf-8')).items(): |
116 | + for pkg, jobs in json.loads(response.read().decode('utf-8') or '{}').items(): |
117 | for handle in jobs: |
118 | for codename in jobs[handle]: |
119 | for arch, jobinfo in jobs[handle][codename].items(): |
120 | @@ -101,12 +101,12 @@ def get_running(response, series=None, ppa=None) -> Iterator[Job]: |
121 | ppas = jobinfo[0].get('ppas', None) |
122 | submit_time = jobinfo[1] |
123 | job = Job(0, submit_time, pkg, codename, arch, triggers, ppas) |
124 | - if (series and (series != job.series)) or (ppa and (ppa not in job.ppas)): |
125 | + if (releases and (job.series not in releases)) or (ppa and (ppa not in job.ppas)): |
126 | continue |
127 | yield job |
128 | |
129 | |
130 | -def get_waiting(response, series=None, ppa=None) -> Iterator[Job]: |
131 | +def get_waiting(response, releases=None, ppa=None) -> Iterator[Job]: |
132 | """Returns iterator of queued autopkgtests for given criteria |
133 | |
134 | Filters the list of autopkgtest jobs waiting for execution by the |
135 | @@ -116,12 +116,12 @@ def get_waiting(response, series=None, ppa=None) -> Iterator[Job]: |
136 | available results for that series or ppa. |
137 | |
138 | :param HTTPResponse response: Context manager; the response from urlopen() |
139 | - :param str series: The Ubuntu release codename criteria, or None. |
140 | + :param list[str] releases: The Ubuntu series codename(s), or None. |
141 | :param str ppa: The PPA address criteria, or None. |
142 | :rtype: Iterator[Job] |
143 | :returns: Currently waiting jobs, if any, or an empty list on error |
144 | """ |
145 | - for _, queue in json.loads(response.read().decode('utf-8')).items(): |
146 | + for _, queue in json.loads(response.read().decode('utf-8') or '{}').items(): |
147 | for codename in queue: |
148 | for arch in queue[codename]: |
149 | n = 0 |
150 | @@ -134,42 +134,42 @@ def get_waiting(response, series=None, ppa=None) -> Iterator[Job]: |
151 | triggers = jobinfo.get('triggers', None) |
152 | ppas = jobinfo.get('ppas', None) |
153 | job = Job(n, None, pkg, codename, arch, triggers, ppas) |
154 | - if (series and (series != job.series)) or (ppa and (ppa not in job.ppas)): |
155 | + if (releases and (job.series not in releases)) or (ppa and (ppa not in job.ppas)): |
156 | continue |
157 | yield job |
158 | |
159 | |
160 | def show_running(jobs): |
161 | """Prints the active (running and waiting) tests""" |
162 | - rformat = " %-8s %-40s %-8s %-8s %-40s %s" |
163 | + rformat = "%-8s %-40s %-8s %-8s %-40s %s" |
164 | |
165 | n = 0 |
166 | for n, e in enumerate(jobs, start=1): |
167 | if n == 1: |
168 | - print("Running:") |
169 | + print("* Running:") |
170 | ppa_str = ','.join(e.ppas) |
171 | trig_str = ','.join(e.triggers) |
172 | - print(rformat % ("time", "pkg", "release", "arch", "ppa", "trigger")) |
173 | - print(rformat % (str(e.submit_time), e.source_package, e.series, e.arch, ppa_str, trig_str)) |
174 | + print(" # " + rformat % ("time", "pkg", "release", "arch", "ppa", "trigger")) |
175 | + print(" - " + rformat % (str(e.submit_time), e.source_package, e.series, e.arch, ppa_str, trig_str)) |
176 | if n == 0: |
177 | - print("Running: (none)") |
178 | + print("* Running: (none)") |
179 | |
180 | |
181 | def show_waiting(jobs): |
182 | """Prints the active (running and waiting) tests""" |
183 | - rformat = " %-8s %-40s %-8s %-8s %-40s %s" |
184 | + rformat = "%-8s %-40s %-8s %-8s %-40s %s" |
185 | |
186 | n = 0 |
187 | for n, e in enumerate(jobs, start=1): |
188 | if n == 1: |
189 | - print("Waiting:") |
190 | - print(rformat % ("Q-num", "pkg", "release", "arch", "ppa", "trigger")) |
191 | + print("* Waiting:") |
192 | + print(" # " + rformat % ("Q-num", "pkg", "release", "arch", "ppa", "trigger")) |
193 | |
194 | ppa_str = ','.join(e.ppas) |
195 | trig_str = ','.join(e.triggers) |
196 | - print(rformat % (e.number, e.source_package, e.series, e.arch, ppa_str, trig_str)) |
197 | + print(" - " + rformat % (e.number, e.source_package, e.series, e.arch, ppa_str, trig_str)) |
198 | if n == 0: |
199 | - print("Waiting: (none)") |
200 | + print("* Waiting: (none)") |
201 | |
202 | |
203 | if __name__ == "__main__": |
204 | @@ -202,11 +202,11 @@ if __name__ == "__main__": |
205 | |
206 | print("running:") |
207 | response = urlopen(f"file://{root_dir}/tests/data/running-20220822.json") |
208 | - for job in get_running(response, 'kinetic', ppa): |
209 | + for job in get_running(response, ['kinetic'], ppa): |
210 | print(job) |
211 | print() |
212 | |
213 | print("waiting:") |
214 | response = urlopen(f"file://{root_dir}/tests/data/queues-20220822.json") |
215 | - for job in get_waiting(response, 'kinetic', ppa): |
216 | + for job in get_waiting(response, ['kinetic'], ppa): |
217 | print(job) |
218 | diff --git a/ppa/ppa.py b/ppa/ppa.py |
219 | index 4140873..7b6f16f 100755 |
220 | --- a/ppa/ppa.py |
221 | +++ b/ppa/ppa.py |
222 | @@ -8,6 +8,8 @@ |
223 | # Released under GNU GPLv2 or later, read the file 'LICENSE.GPLv2+' for |
224 | # more information. |
225 | |
226 | +"""A wrapper around a Launchpad Personal Package Archive object.""" |
227 | + |
228 | import re |
229 | |
230 | from textwrap import indent |
231 | @@ -32,7 +34,7 @@ class PpaDoesNotExist(BaseException): |
232 | """Printable error message. |
233 | |
234 | :rtype str: |
235 | - :return: Error message about the failure |
236 | + :return: Error message about the failure. |
237 | """ |
238 | if self.message: |
239 | return self.message |
240 | @@ -40,7 +42,7 @@ class PpaDoesNotExist(BaseException): |
241 | |
242 | |
243 | class Ppa: |
244 | - """Encapsulates data needed to access and conveniently wrap a PPA |
245 | + """Encapsulates data needed to access and conveniently wrap a PPA. |
246 | |
247 | This object proxies a PPA, allowing lazy initialization and caching |
248 | of data from the remote. |
249 | @@ -88,6 +90,7 @@ class Ppa: |
250 | return f"{self.team_name}/{self.name}" |
251 | |
252 | @property |
253 | + @lru_cache |
254 | def archive(self): |
255 | """Retrieves the LP Archive object from the Launchpad service. |
256 | |
257 | @@ -101,6 +104,15 @@ class Ppa: |
258 | except NotFound: |
259 | raise PpaDoesNotExist(self.ppa_name, self.team_name) |
260 | |
261 | + @lru_cache |
262 | + def exists(self) -> bool: |
263 | + """Returns true if the PPA exists in Launchpad.""" |
264 | + try: |
265 | + self.archive |
266 | + return True |
267 | + except PpaDoesNotExist: |
268 | + return False |
269 | + |
270 | @property |
271 | @lru_cache |
272 | def address(self): |
273 | @@ -157,6 +169,7 @@ class Ppa: |
274 | return retval and self.archive.description == description |
275 | |
276 | @property |
277 | + @lru_cache |
278 | def architectures(self): |
279 | """Returns the architectures configured to build packages in the PPA. |
280 | |
281 | @@ -408,7 +421,7 @@ def get_das(distro, series_name, arch_name): |
282 | |
283 | |
284 | def get_ppa(lp, config): |
285 | - """Load the specified PPA from Launchpad |
286 | + """Load the specified PPA from Launchpad. |
287 | |
288 | :param Lp lp: The Launchpad wrapper object. |
289 | :param dict config: Configuration param:value map. |
290 | diff --git a/ppa/ppa_group.py b/ppa/ppa_group.py |
291 | index ce1c0d1..622cf76 100755 |
292 | --- a/ppa/ppa_group.py |
293 | +++ b/ppa/ppa_group.py |
294 | @@ -8,12 +8,14 @@ |
295 | # Released under GNU GPLv2 or later, read the file 'LICENSE.GPLv2+' for |
296 | # more information. |
297 | |
298 | -from .ppa import Ppa |
299 | -from .text import o2str |
300 | +"""A team or person that owns one or more PPAs in Launchpad.""" |
301 | |
302 | from functools import lru_cache |
303 | from lazr.restfulclient.errors import BadRequest |
304 | |
305 | +from .ppa import Ppa |
306 | +from .text import o2str |
307 | + |
308 | |
309 | class PpaAlreadyExists(BaseException): |
310 | """Exception indicating a PPA operation could not be performed.""" |
311 | @@ -28,10 +30,10 @@ class PpaAlreadyExists(BaseException): |
312 | self.message = message |
313 | |
314 | def __str__(self): |
315 | - """Printable error message |
316 | + """Printable error message. |
317 | |
318 | :rtype str: |
319 | - :return: Error message about the failure |
320 | + :return: Error message about the failure. |
321 | """ |
322 | if self.message: |
323 | return self.message |
324 | @@ -51,7 +53,11 @@ class PpaGroup: |
325 | :param launchpadlib.service service: The Launchpad service object. |
326 | :param str name: Launchpad username or team name. |
327 | """ |
328 | - assert service is not None |
329 | + if not service: |
330 | + raise ValueError("undefined service.") |
331 | + if not name: |
332 | + raise ValueError("undefined name.") |
333 | + |
334 | self.service = service |
335 | self.name = name |
336 | |
337 | @@ -116,9 +122,10 @@ class PpaGroup: |
338 | @property |
339 | @lru_cache |
340 | def ppas(self): |
341 | - """Generator to access the PPAs in this group |
342 | + """Generator to access the PPAs in this group. |
343 | + |
344 | :rtype: Iterator[ppa.Ppa] |
345 | - :returns: Each PPA in the group as a ppa.Ppa object |
346 | + :returns: Each PPA in the group as a ppa.Ppa object. |
347 | """ |
348 | for lp_ppa in self.team.ppas: |
349 | if '-deletedppa' in lp_ppa.name: |
350 | @@ -129,6 +136,7 @@ class PpaGroup: |
351 | @lru_cache |
352 | def get(self, ppa_name): |
353 | """Provides a Ppa for the named ppa. |
354 | + |
355 | :rtype: ppa.Ppa |
356 | :returns: A Ppa object describing the named ppa. |
357 | """ |
358 | diff --git a/ppa/result.py b/ppa/result.py |
359 | index 012165d..db0af48 100755 |
360 | --- a/ppa/result.py |
361 | +++ b/ppa/result.py |
362 | @@ -13,7 +13,7 @@ |
363 | |
364 | import re |
365 | import urllib.request |
366 | -from functools import lru_cache, cached_property |
367 | +from functools import lru_cache |
368 | from typing import List, Iterator |
369 | import gzip |
370 | import time |
371 | @@ -65,14 +65,16 @@ class Result: |
372 | :rtype: str |
373 | :returns: Printable summary of the object. |
374 | """ |
375 | - return f"{self.source} on {self.series} for {self.arch} @ {self.timestamp}" |
376 | + pad = ' ' * (1 + abs(len('ppc64el') - len(self.arch))) |
377 | + return f"{self.source} on {self.series} for {self.arch}{pad}@ {self.timestamp}" |
378 | |
379 | @property |
380 | def timestamp(self) -> str: |
381 | """Formats the result's completion time as a string.""" |
382 | return time.strftime("%d.%m.%y %H:%M:%S", self.time) |
383 | |
384 | - @cached_property |
385 | + @property |
386 | + @lru_cache |
387 | def log(self) -> str: |
388 | """Returns log contents for results, downloading if necessary. |
389 | |
390 | @@ -103,7 +105,8 @@ class Result: |
391 | return None |
392 | |
393 | # TODO: Merge triggers and get_triggers() |
394 | - @cached_property |
395 | + @property |
396 | + @lru_cache |
397 | def triggers(self) -> List[str]: |
398 | """Returns package/version parameters used to generate this Result. |
399 | |
400 | @@ -165,7 +168,8 @@ class Result: |
401 | subtests.append(Subtest(line)) |
402 | return subtests |
403 | |
404 | - @cached_property |
405 | + @property |
406 | + @lru_cache |
407 | def status(self) -> str: |
408 | """Returns overall status of all subtests |
409 | |
410 | @@ -187,7 +191,8 @@ class Result: |
411 | return 'FAIL' |
412 | return 'PASS' |
413 | |
414 | - @cached_property |
415 | + @property |
416 | + @lru_cache |
417 | def status_icon(self) -> str: |
418 | """Unicode symbol corresponding to test's overall status. |
419 | |
420 | diff --git a/ppa/subtest.py b/ppa/subtest.py |
421 | index 2f18190..8d871c4 100755 |
422 | --- a/ppa/subtest.py |
423 | +++ b/ppa/subtest.py |
424 | @@ -11,7 +11,7 @@ |
425 | |
426 | """An individual DEP8 test run""" |
427 | |
428 | -from functools import cached_property |
429 | +from functools import lru_cache |
430 | |
431 | |
432 | class Subtest: |
433 | @@ -55,7 +55,8 @@ class Subtest: |
434 | """ |
435 | return f"{self.desc:25} {self.status:6} {self.status_icon}" |
436 | |
437 | - @cached_property |
438 | + @property |
439 | + @lru_cache |
440 | def desc(self) -> str: |
441 | """The descriptive text for the given subtest. |
442 | |
443 | @@ -64,7 +65,8 @@ class Subtest: |
444 | """ |
445 | return next(iter(self._line.split()), '') |
446 | |
447 | - @cached_property |
448 | + @property |
449 | + @lru_cache |
450 | def status(self) -> str: |
451 | """The success or failure of the given subtest. |
452 | |
453 | @@ -76,7 +78,8 @@ class Subtest: |
454 | return k |
455 | return 'UNKNOWN' |
456 | |
457 | - @cached_property |
458 | + @property |
459 | + @lru_cache |
460 | def status_icon(self) -> str: |
461 | """Unicode symbol corresponding to subtest's status. |
462 | |
463 | diff --git a/ppa/text.py b/ppa/text.py |
464 | index b1e1453..ebd3ec3 100644 |
465 | --- a/ppa/text.py |
466 | +++ b/ppa/text.py |
467 | @@ -69,8 +69,8 @@ def o2str(obj): |
468 | |
469 | @lru_cache |
470 | def to_bool(value): |
471 | - """ |
472 | - Converts 'something' to boolean. Raises exception for invalid formats |
473 | + """Converts 'something' to boolean. Raises exception for invalid formats. |
474 | + |
475 | Possible True values: 1, True, '1', 'TRue', 'yes', 'y', 't' |
476 | Possible False values: 0, False, None, [], {}, '', '0', 'faLse', 'no', 'n', 'f', 0.0 |
477 | """ |
478 | @@ -116,31 +116,16 @@ def o2float(value): |
479 | raise |
480 | |
481 | |
482 | +def ansi_hyperlink(url, text): |
483 | + """Formats text into a hyperlink using ANSI escape codes. |
484 | + |
485 | + :param str url: The linked action to load in a web browser. |
486 | + :param str text: The visible text to show. |
487 | + :rtype: str |
488 | + :returns: ANSI escape code sequence to display the hyperlink. |
489 | + """ |
490 | + return f"\u001b]8;;{url}\u001b\\{text}\u001b]8;;\u001b\\" |
491 | + |
492 | + |
493 | if __name__ == "__main__": |
494 | - test_cases = [ |
495 | - ('true', True), |
496 | - ('t', True), |
497 | - ('yes', True), |
498 | - ('y', True), |
499 | - ('1', True), |
500 | - ('false', False), |
501 | - ('f', False), |
502 | - ('no', False), |
503 | - ('n', False), |
504 | - ('0', False), |
505 | - ('', False), |
506 | - (1, True), |
507 | - (0, False), |
508 | - (1.0, True), |
509 | - (0.0, False), |
510 | - ([], False), |
511 | - ({}, False), |
512 | - ((), False), |
513 | - ([1], True), |
514 | - ({1: 2}, True), |
515 | - ((1,), True), |
516 | - (None, False), |
517 | - (object(), True), |
518 | - ] |
519 | - for test, expected in test_cases: |
520 | - assert to_bool(test) == expected, f"to_bool({test}) failed to return {expected}" |
521 | + print(ansi_hyperlink("https://launchpad.net/ppa-dev-tools", "ppa-dev-tools")) |
522 | diff --git a/ppa/trigger.py b/ppa/trigger.py |
523 | index ae00b44..72f8ba6 100755 |
524 | --- a/ppa/trigger.py |
525 | +++ b/ppa/trigger.py |
526 | @@ -11,7 +11,7 @@ |
527 | |
528 | """A directive to run a DEP8 test against a source package""" |
529 | |
530 | -from functools import cached_property |
531 | +from functools import lru_cache |
532 | from urllib.parse import urlencode |
533 | |
534 | from .constants import URL_AUTOPKGTEST |
535 | @@ -60,7 +60,8 @@ class Trigger: |
536 | """ |
537 | return f"{self.package}/{self.version}" |
538 | |
539 | - @cached_property |
540 | + @property |
541 | + @lru_cache |
542 | def history_url(self) -> str: |
543 | """Renders the trigger as a URL to the job history. |
544 | |
545 | @@ -74,7 +75,8 @@ class Trigger: |
546 | pkg_str = f"{prefix}/{self.package}" |
547 | return f"{URL_AUTOPKGTEST}/packages/{pkg_str}/{self.series}/{self.arch}" |
548 | |
549 | - @cached_property |
550 | + @property |
551 | + @lru_cache |
552 | def action_url(self) -> str: |
553 | """Renders the trigger as a URL to start running the test. |
554 | |
555 | diff --git a/scripts/ppa b/scripts/ppa |
556 | index 04abd4c..09ba1e7 100755 |
557 | --- a/scripts/ppa |
558 | +++ b/scripts/ppa |
559 | @@ -51,7 +51,7 @@ import sys |
560 | import time |
561 | import argparse |
562 | from inspect import currentframe |
563 | -from distro_info import UbuntuDistroInfo |
564 | +from distro_info import UbuntuDistroInfo, DistroDataOutdated |
565 | |
566 | try: |
567 | from ruamel import yaml |
568 | @@ -88,11 +88,11 @@ from ppa.result import ( |
569 | Result, |
570 | get_results |
571 | ) |
572 | -from ppa.text import o2str |
573 | +from ppa.text import o2str, ansi_hyperlink |
574 | from ppa.trigger import Trigger |
575 | |
576 | import ppa.debug |
577 | -from ppa.debug import dbg, warn |
578 | +from ppa.debug import dbg, warn, error |
579 | |
580 | |
581 | def UNIMPLEMENTED(): |
582 | @@ -192,6 +192,18 @@ def create_arg_parser(): |
583 | tests_parser.add_argument('ppa_name', metavar='ppa-name', |
584 | action='store', |
585 | help="Name of the PPA to view tests") |
586 | + tests_parser.add_argument('-a', '--arches', '--arch', '--architectures', |
587 | + dest="architectures", action='store', |
588 | + help="Comma-separated list of hardware architectures to include in triggers") |
589 | + tests_parser.add_argument('-r', '--releases', '--release', |
590 | + dest="releases", action='store', |
591 | + help="Comma-separated list of Ubuntu release codenames to show triggers for") |
592 | + tests_parser.add_argument('-p', '--packages', '--package', |
593 | + dest="packages", action='store', |
594 | + help="Comma-separated list of source package names to show triggers for") |
595 | + tests_parser.add_argument('-L', '--show-urls', |
596 | + dest='show_urls', action='store_true', |
597 | + help="Display unformatted trigger action URLs") |
598 | |
599 | # Wait Command |
600 | wait_parser = subparser.add_parser('wait', help='wait help') |
601 | @@ -260,6 +272,13 @@ def create_config(lp, args): |
602 | else: |
603 | warn(f"Invalid architectures '{args.architectures}'") |
604 | return None |
605 | + elif args.command == 'tests': |
606 | + if args.releases is not None: |
607 | + if args.releases: |
608 | + config['releases'] = args.releases.split(',') |
609 | + else: |
610 | + warn(f"Invalid releases '{args.releases}'") |
611 | + return None |
612 | |
613 | return config |
614 | |
615 | @@ -550,49 +569,88 @@ def command_tests(lp, config): |
616 | if not lp: |
617 | return 1 |
618 | |
619 | - # Show tests only from the current development release |
620 | - udi = UbuntuDistroInfo() |
621 | - release = udi.devel() |
622 | + releases = config.get('releases', None) |
623 | + if releases is None: |
624 | + udi = UbuntuDistroInfo() |
625 | + try: |
626 | + # Show tests only from the current development release |
627 | + releases = [ udi.devel() ] |
628 | + except DistroDataOutdated as e: |
629 | + # If no development release defined, use the current active release |
630 | + warn(f"Devel release is undefined; assuming stable release instead.") |
631 | + dbg(f"({e})", wrap=72, prefix=' - ', indent=' ') |
632 | + releases = [ udi.stable() ] |
633 | + |
634 | + packages = config.get('packages', None) |
635 | + |
636 | + ppa = get_ppa(lp, config) |
637 | + if not ppa.exists(): |
638 | + error(f"PPA {ppa.name} does not exist for user {ppa.team_name}") |
639 | + return 1 |
640 | + |
641 | + architectures = config.get('architectures', ARCHES_AUTOPKGTEST) |
642 | |
643 | try: |
644 | - ppa = get_ppa(lp, config) |
645 | + # Triggers |
646 | + print("* Triggers:") |
647 | for source_pub in ppa.get_source_publications(): |
648 | - # Triggers |
649 | - for arch in ARCHES_AUTOPKGTEST: |
650 | - trigger = Trigger( |
651 | - package=source_pub.source_package_name, |
652 | - version=source_pub.source_package_version, |
653 | - arch=arch, |
654 | - series=source_pub.distro_series.name, |
655 | - ppa=ppa) |
656 | - print(trigger.action_url) |
657 | + series = source_pub.distro_series.name |
658 | + if series not in releases: |
659 | + continue |
660 | + pkg = source_pub.source_package_name |
661 | + if packages and (pkg not in packages): |
662 | + continue |
663 | + ver = source_pub.source_package_version |
664 | + url = f"https://launchpad.net/ubuntu/+source/{pkg}/{ver}" |
665 | + source_hyperlink = ansi_hyperlink(url, f"{pkg}/{ver}") |
666 | + print(f" - Source {source_hyperlink}: {source_pub.status}") |
667 | + triggers = [Trigger(pkg, ver, arch, series, ppa) for arch in architectures] |
668 | + |
669 | + if config.get("show_urls"): |
670 | + for trigger in triggers: |
671 | + print(f" + {trigger.arch}: {trigger.action_url}♻️ ") |
672 | + for trigger in triggers: |
673 | + print(f" + {trigger.arch}: {trigger.action_url}💍") |
674 | + |
675 | + else: |
676 | + for trigger in triggers: |
677 | + pad = ' ' * (1 + abs(len('ppc64el') - len(trigger.arch))) |
678 | + basic_trig = ansi_hyperlink(trigger.action_url, f"Trigger basic @{trigger.arch}♻️ ") |
679 | + all_proposed_trig = ansi_hyperlink(trigger.action_url + "&all-proposed=1", |
680 | + f"Trigger all-proposed @{trigger.arch}💍") |
681 | + print(f" + " + pad.join([basic_trig, all_proposed_trig])) |
682 | |
683 | # Results |
684 | - base_results_fmt = f"{URL_AUTOPKGTEST}/results/autopkgtest-%s-%s-%s/" |
685 | - base_results_url = base_results_fmt % (release, ppa.team_name, ppa.name) |
686 | - url = f"{base_results_url}?format=plain" |
687 | - response = open_url(url) |
688 | - if response: |
689 | - for result in get_results(response, base_results_url, arches=ARCHES_AUTOPKGTEST): |
690 | - print(f"* {result} {result.status_icon}") |
691 | - print(f" - Triggers: " + ', '.join([str(r) for r in result.get_triggers()])) |
692 | - if result.status != 'PASS': |
693 | - print(f" - Status: {result.status}") |
694 | - print(f" - Log: {result.url}") |
695 | - for subtest in result.get_subtests(): |
696 | - print(f" - {subtest}") |
697 | - print() |
698 | + print("* Results:") |
699 | + for release in releases: |
700 | + base_results_fmt = f"{URL_AUTOPKGTEST}/results/autopkgtest-%s-%s-%s/" |
701 | + base_results_url = base_results_fmt % (release, ppa.team_name, ppa.name) |
702 | + url = f"{base_results_url}?format=plain" |
703 | + response = open_url(url) |
704 | + if response: |
705 | + trigger_sets = {} |
706 | + for result in get_results(response, base_results_url, arches=architectures): |
707 | + trigger = ', '.join([str(r) for r in result.get_triggers()]) |
708 | + trigger_sets.setdefault(trigger, '') |
709 | + trigger_sets[trigger] += f" + {result.status_icon} {result}\n" |
710 | + if result.status != 'PASS': |
711 | + trigger_sets[trigger] += f" • Status: {result.status}\n" |
712 | + trigger_sets[trigger] += f" • Log: {result.url}\n" |
713 | + for subtest in result.get_subtests(): |
714 | + trigger_sets[trigger] += f" • {subtest}\n" |
715 | + for trigger, result in trigger_sets.items(): |
716 | + print(f" - {trigger}\n{result}") |
717 | |
718 | # Running Queue |
719 | response = open_url(f"{URL_AUTOPKGTEST}/static/running.json", "running autopkgtests") |
720 | if response: |
721 | - show_running(sorted(get_running(response, series=release, ppa=str(ppa)), |
722 | - key=lambda k: str(k.submit_time))) |
723 | + show_running(sorted(get_running(response, releases=releases, ppa=str(ppa)), |
724 | + key=lambda k: str(k.submit_time))) |
725 | |
726 | # Waiting Queue |
727 | response = open_url(f"{URL_AUTOPKGTEST}/queues.json", "waiting autopkgtests") |
728 | if response: |
729 | - show_waiting(get_waiting(response, series=release, ppa=str(ppa))) |
730 | + show_waiting(get_waiting(response, releases=releases, ppa=str(ppa))) |
731 | |
732 | return os.EX_OK |
733 | except KeyboardInterrupt: |
734 | @@ -640,7 +698,7 @@ def main(args): |
735 | return func(lp, config, param) |
736 | return func(lp, config) |
737 | except KeyError: |
738 | - parser.error("No such command {}".format(args.command)) |
739 | + parser.error(f"No such command {args.command}") |
740 | return 1 |
741 | |
742 | |
743 | diff --git a/tests/test_job.py b/tests/test_job.py |
744 | index 78a12c7..1d1708b 100644 |
745 | --- a/tests/test_job.py |
746 | +++ b/tests/test_job.py |
747 | @@ -81,7 +81,7 @@ def test_get_running(): |
748 | '"Log Output Here"' |
749 | '] } } } }') |
750 | fake_response = RequestResponseMock(json_text) |
751 | - job = next(get_running(fake_response, series='focal', ppa='ppa:me/myppa')) |
752 | + job = next(get_running(fake_response, releases=['focal'], ppa='ppa:me/myppa')) |
753 | assert repr(job) == "Job(source_package='mypackage', series='focal', arch='arm64')" |
754 | assert job.triggers == ["yourpackage/1.2.3"] |
755 | assert job.ppas == ["ppa:me/myppa"] |
756 | @@ -100,7 +100,7 @@ def test_get_waiting(): |
757 | ' \\"triggers\\": [ \\"c/3.2-1\\", \\"d/2-2\\" ] }"' |
758 | '] } } }') |
759 | fake_response = RequestResponseMock(json_text) |
760 | - job = next(get_waiting(fake_response, series='focal', ppa='ppa:me/myppa')) |
761 | + job = next(get_waiting(fake_response, releases=['focal'], ppa='ppa:me/myppa')) |
762 | assert job |
763 | assert job.source_package == "b" |
764 | assert job.ppas == ['ppa:me/myppa'] |
765 | diff --git a/tests/test_ppa_group.py b/tests/test_ppa_group.py |
766 | index 63907fd..f8aa8db 100644 |
767 | --- a/tests/test_ppa_group.py |
768 | +++ b/tests/test_ppa_group.py |
769 | @@ -23,9 +23,14 @@ from tests.helpers import PersonMock, LaunchpadMock, LpServiceMock |
770 | |
771 | def test_object(): |
772 | """Checks that PpaGroup objects can be instantiated.""" |
773 | - ppa_group = PpaGroup(service=LpServiceMock(), name=None) |
774 | + ppa_group = PpaGroup(service=LpServiceMock(), name='test-ppa') |
775 | assert ppa_group |
776 | |
777 | + with pytest.raises(ValueError): |
778 | + ppa_group = PpaGroup(service=LpServiceMock(), name=None) |
779 | + with pytest.raises(ValueError): |
780 | + ppa_group = PpaGroup(service=None, name='test-ppa') |
781 | + |
782 | |
783 | def test_create_ppa(): |
784 | """Checks that PpaGroups can create PPAs.""" |
785 | diff --git a/tests/test_result.py b/tests/test_result.py |
786 | index 8c498a4..d928efb 100644 |
787 | --- a/tests/test_result.py |
788 | +++ b/tests/test_result.py |
789 | @@ -41,7 +41,7 @@ def test_str(): |
790 | """Checks Result object textual presentation.""" |
791 | timestamp = time.strptime('20030201_040506', "%Y%m%d_%H%M%S") |
792 | result = Result('url', timestamp, 'b', 'c', 'd') |
793 | - assert f"{result}" == 'd on b for c @ 01.02.03 04:05:06' |
794 | + assert f"{result}" == 'd on b for c @ 01.02.03 04:05:06' |
795 | |
796 | |
797 | def test_timestamp(): |
798 | diff --git a/tests/test_scripts_ppa.py b/tests/test_scripts_ppa.py |
799 | index 812b4b5..51a0755 100644 |
800 | --- a/tests/test_scripts_ppa.py |
801 | +++ b/tests/test_scripts_ppa.py |
802 | @@ -112,7 +112,7 @@ def test_create_arg_parser(): |
803 | args.debug = None |
804 | |
805 | # Check -q, --dry-run |
806 | - args = parser.parse_args(['cmd', '--dry-run']) |
807 | + args = parser.parse_args(['--dry-run', 'status', 'test-ppa']) |
808 | assert args.dry_run == True |
809 | args.dry_run = None |
810 | |
811 | @@ -159,11 +159,18 @@ def test_create_arg_parser_create(): |
812 | args = parser.parse_args(['create', 'test-ppa']) |
813 | assert args.ppa_name == 'test-ppa' |
814 | |
815 | - # Check -a, --arch, --arches, --architectures |
816 | + # Check that command args can come before or after the ppa name |
817 | args = parser.parse_args(['create', 'test-ppa', '-a', 'x']) |
818 | assert args.architectures == 'x' |
819 | args.architectures = None |
820 | + args = parser.parse_args(['create', '-a', 'x', 'test-ppa']) |
821 | + assert args.architectures == 'x' |
822 | + args.architectures = None |
823 | |
824 | + # Check -a, --arch, --arches, --architectures |
825 | + args = parser.parse_args(['create', 'test-ppa', '-a', 'x']) |
826 | + assert args.architectures == 'x' |
827 | + args.architectures = None |
828 | args = parser.parse_args(['create', 'test-ppa', '--arch', 'x']) |
829 | assert args.architectures == 'x' |
830 | args.architectures = None |
831 | @@ -175,6 +182,72 @@ def test_create_arg_parser_create(): |
832 | args.architectures = None |
833 | |
834 | |
835 | +def test_create_arg_parser_tests(): |
836 | + """Checks argument parsing for the 'tests' command.""" |
837 | + parser = script.create_arg_parser() |
838 | + |
839 | + # Check ppa_name |
840 | + args = parser.parse_args(['tests', 'test-ppa']) |
841 | + assert args.ppa_name == 'test-ppa' |
842 | + |
843 | + # Check -a, --arch, --arches, --architectures |
844 | + args = parser.parse_args(['tests', 'test-ppa', '-a', 'x']) |
845 | + assert args.architectures == 'x' |
846 | + args.architectures = None |
847 | + |
848 | + args = parser.parse_args(['tests', 'test-ppa', '--arch', 'x']) |
849 | + assert args.architectures == 'x' |
850 | + args.architectures = None |
851 | + args = parser.parse_args(['tests', 'test-ppa', '--arches', 'x']) |
852 | + assert args.architectures == 'x' |
853 | + args.architectures = None |
854 | + args = parser.parse_args(['tests', 'test-ppa', '--architectures', 'a,b,c']) |
855 | + assert args.architectures == 'a,b,c' |
856 | + args.architectures = None |
857 | + |
858 | + # Check -r, --release, --releases |
859 | + args = parser.parse_args(['tests', 'test-ppa', '-r', 'x']) |
860 | + assert args.releases == 'x' |
861 | + args.releases = None |
862 | + args = parser.parse_args(['tests', 'test-ppa', '--release', 'x']) |
863 | + assert args.releases == 'x' |
864 | + args.releases = None |
865 | + args = parser.parse_args(['tests', 'test-ppa', '--releases', 'x']) |
866 | + assert args.releases == 'x' |
867 | + args.releases = None |
868 | + args = parser.parse_args(['tests', 'test-ppa', '--releases', 'x,y,z']) |
869 | + assert args.releases == 'x,y,z' |
870 | + args.releases = None |
871 | + |
872 | + # Check -p, --package, --packages |
873 | + args = parser.parse_args(['tests', 'tests-ppa', '-p', 'x']) |
874 | + assert args.packages == 'x' |
875 | + args.packages = None |
876 | + args = parser.parse_args(['tests', 'tests-ppa', '--package', 'x']) |
877 | + assert args.packages == 'x' |
878 | + args.packages = None |
879 | + args = parser.parse_args(['tests', 'tests-ppa', '--packages', 'x']) |
880 | + assert args.packages == 'x' |
881 | + args.packages = None |
882 | + args = parser.parse_args(['tests', 'tests-ppa', '--packages', 'x,y,z']) |
883 | + assert args.packages == 'x,y,z' |
884 | + args.packages = None |
885 | + |
886 | + # Check --show-urls, --show-url, -L |
887 | + args = parser.parse_args(['tests', 'tests-ppa']) |
888 | + assert args.show_urls == False |
889 | + args.show_urls = None |
890 | + args = parser.parse_args(['tests', 'tests-ppa', '--show-urls']) |
891 | + assert args.show_urls == True |
892 | + args.show_urls = None |
893 | + args = parser.parse_args(['tests', 'tests-ppa', '--show-url']) |
894 | + assert args.show_urls == True |
895 | + args.show_urls = None |
896 | + args = parser.parse_args(['tests', 'tests-ppa', '-L']) |
897 | + assert args.show_urls == True |
898 | + args.show_urls = None |
899 | + |
900 | + |
901 | @pytest.mark.xfail(reason="Unimplemented") |
902 | def test_create_config(): |
903 | # args = [] |
904 | diff --git a/tests/test_text.py b/tests/test_text.py |
905 | new file mode 100644 |
906 | index 0000000..57ce979 |
907 | --- /dev/null |
908 | +++ b/tests/test_text.py |
909 | @@ -0,0 +1,54 @@ |
910 | +#!/usr/bin/env python3 |
911 | +# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- |
912 | + |
913 | +# Author: Bryce Harrington <bryce@canonical.com> |
914 | +# |
915 | +# Copyright (C) 2022 Bryce W. Harrington |
916 | +# |
917 | +# Released under GNU GPLv2 or later, read the file 'LICENSE.GPLv2+' for |
918 | +# more information. |
919 | + |
920 | +import os |
921 | +import sys |
922 | + |
923 | +import pytest |
924 | + |
925 | +sys.path.insert(0, os.path.realpath( |
926 | + os.path.join(os.path.dirname(os.path.realpath(__file__)), ".."))) |
927 | + |
928 | +import ppa.text |
929 | + |
930 | +@pytest.mark.parametrize('input, expected', [ |
931 | + ('true', True), |
932 | + ('t', True), |
933 | + ('yes', True), |
934 | + ('y', True), |
935 | + ('1', True), |
936 | + ('false', False), |
937 | + ('f', False), |
938 | + ('no', False), |
939 | + ('n', False), |
940 | + ('0', False), |
941 | + ('', False), |
942 | + (1, True), |
943 | + (0, False), |
944 | + (1.0, True), |
945 | + (0.0, False), |
946 | + ((), False), |
947 | + ((1,), True), |
948 | + (None, False), |
949 | + (object(), True), |
950 | +]) |
951 | +def test_to_bool(input, expected): |
952 | + """Check that the given input produces the expected true/false result. |
953 | + |
954 | + :param * input: Any available type to be converted to boolean. |
955 | + :param bool expected: The True or False result to expect. |
956 | + """ |
957 | + assert ppa.text.to_bool(input) == expected |
958 | + |
959 | + |
960 | +def test_ansi_hyperlink(): |
961 | + """Check that text can be linked with a url.""" |
962 | + assert ppa.text.ansi_hyperlink("xxx", "yyy") == "\u001b]8;;xxx\u001b\\yyy\u001b]8;;\u001b\\" |
963 | + |
Hi Bryce! Nice work :)
This LGTM. I added a few inline comments below. As usual, feel free to ignore/act upon them as you please, since they are mostly minor nitpicks.
For large patch sets, it would be nice to have refactoring changes in their own commits. introduction, but also refactors a few code snippets to change a param name. Splitting this in different commits could help reviewers or any future change analysis that may be needed.
For instance, bfd3865 is described as a test change/