Merge ppa-dev-tools:results-class into ppa-dev-tools:main
- Git
- lp:ppa-dev-tools
- results-class
- Merge into main
Status: | Merged |
---|---|
Merged at revision: | 801f354619b8a21dee7350b01fcd32e5aef387a3 |
Proposed branch: | ppa-dev-tools:results-class |
Merge into: | ppa-dev-tools:main |
Diff against target: |
786 lines (+673/-6) 10 files modified
.flake8 (+1/-0) AUTHORS.md (+2/-0) ppa/result.py (+265/-0) ppa/subtest.py (+109/-0) ppa/trigger.py (+80/-0) tests/test_io.py (+2/-3) tests/test_job.py (+7/-3) tests/test_result.py (+66/-0) tests/test_subtest.py (+86/-0) tests/test_trigger.py (+55/-0) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Athos Ribeiro (community) | Approve | ||
Canonical Server Reporter | Pending | ||
Review via email: mp+429187@code.launchpad.net |
Commit message
Description of the change
Adds the Subtest, Trigger, and Results classes.
I've run lint/flake on these via the check-scripts command from ubuntu-helpers.
$ check-scripts ./ppa/result.py
$ check-scripts ./tests/
...
Each module has a corresponding test that can be invoked via pytest-3, e.g:
$ pytest-3 ./tests/
...
Each module can also be run as a script, as a cheap smoketest:
$ python3 -m ppa.result
Bryce Harrington (bryce) wrote : | # |
Answer & re-question for one of your feedback items, inline.
Athos Ribeiro (athos-ribeiro) wrote : | # |
On Wed, Aug 31, 2022 at 06:40:02PM -0000, Bryce Harrington wrote:
>Answer & re-question for one of your feedback items, inline.
>
>Diff comments:
>
>> diff --git a/tests/test_job.py b/tests/test_job.py
>> index d9a9967..44491c2 100644
>> --- a/tests/test_job.py
>> +++ b/tests/test_job.py
>> @@ -71,9 +74,10 @@ def test_request_url():
>> jobinfo = {
>> 'triggers': ['t/1'],
>> 'ppas': ['ppa:a/b', 'ppa:c/d']
>> - }
>> + }
>> job = Job(0, 'a', 'b', 'c', 'd', jobinfo[
>> - assert job.request_url == "https:/
>> + assert job.request_
>> + assert job.request_
>
>Yeah it was for shortening the length for the lint warning. Normally I'd do the new variables but figured that would make the testing less clear.
>
>My thinking here for two tests is that it now checks a) is the cgi address correctly inserted, and then b) are the args showing up properly. Do you think it'd be better to just check the whole URL?
>
This was just a nitpick to point out that the test changed. To keep the
behavior, i.e, for the commit to be a refactoring only commit, it should
keep checking the whole thing. It should be OK to merge it as is though
:)
>>
>>
>> def test_get_running():
>
>
>--
>https:/
>You are reviewing the proposed merge of ppa-dev-
>
--
Athos Ribeiro
Bryce Harrington (bryce) wrote : | # |
Thanks Athos, yeah while it's not a pure refactor I think splitting the url check to two tests is a good change. I've gone ahead and landed the branch.
Preview Diff
1 | diff --git a/.flake8 b/.flake8 |
2 | index 7da1f96..d6c1210 100644 |
3 | --- a/.flake8 |
4 | +++ b/.flake8 |
5 | @@ -1,2 +1,3 @@ |
6 | [flake8] |
7 | max-line-length = 100 |
8 | +ignore = E402 |
9 | diff --git a/AUTHORS.md b/AUTHORS.md |
10 | index 6138e91..4aef7e4 100644 |
11 | --- a/AUTHORS.md |
12 | +++ b/AUTHORS.md |
13 | @@ -1,2 +1,4 @@ |
14 | Bryce Harrington <bryce@canonical.com> |
15 | Athos Ribeiro <athos.ribeiro@canonical.com> |
16 | +Christian Ehrhardt <christian.ehrhardt@canonical.com> |
17 | +Andy P. Whitcroft <apw@shadowen.org> |
18 | diff --git a/ppa/result.py b/ppa/result.py |
19 | new file mode 100755 |
20 | index 0000000..7a2ca5a |
21 | --- /dev/null |
22 | +++ b/ppa/result.py |
23 | @@ -0,0 +1,265 @@ |
24 | +#!/usr/bin/env python3 |
25 | +# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- |
26 | + |
27 | +# Copyright (C) 2022 Authors |
28 | +# |
29 | +# Released under GNU GPLv2 or later, read the file 'LICENSE.GPLv2+' for |
30 | +# more information. |
31 | +# |
32 | +# Authors: |
33 | +# Bryce Harrington <bryce@canonical.com> |
34 | + |
35 | +"""The completed data from an autopkgtest run""" |
36 | + |
37 | +import re |
38 | +import urllib.request |
39 | +from functools import lru_cache, cached_property |
40 | +from typing import List, Iterator |
41 | +import gzip |
42 | +import time |
43 | + |
44 | +from .subtest import Subtest |
45 | +from .trigger import Trigger |
46 | + |
47 | + |
48 | +class Result: |
49 | + """ |
50 | + The completed data from an autopkgtest run Job. This object |
51 | + provides access to the test run's settings and results. |
52 | + """ |
53 | + VALUES = { |
54 | + 'PASS': "✅", |
55 | + 'FAIL': "❌", |
56 | + 'BAD': "⛔", |
57 | + } |
58 | + |
59 | + def __init__(self, url, time, series, arch, source): |
60 | + """Initializes a new Result object. |
61 | + |
62 | + :param str url: HTTP path to the test log for this result. |
63 | + :param str time: The execution time of the test run. |
64 | + :param str series: The distro release series codename. |
65 | + :param str arch: The architecture for the result. |
66 | + :param str source: |
67 | + """ |
68 | + self.url = url |
69 | + self.time = time |
70 | + self.series = series |
71 | + self.arch = arch |
72 | + self.source = source |
73 | + self.error_message = None |
74 | + self._log = None |
75 | + |
76 | + def __repr__(self) -> str: |
77 | + """Machine-parsable unique representation of object. |
78 | + |
79 | + :rtype: str |
80 | + :returns: Official string representation of the object. |
81 | + """ |
82 | + return (f'{self.__class__.__name__}(' |
83 | + f'url={self.url!r})') |
84 | + |
85 | + def __str__(self) -> str: |
86 | + """Human-readable summary of the object. |
87 | + |
88 | + :rtype: str |
89 | + :returns: Printable summary of the object. |
90 | + """ |
91 | + return f"{self.source} on {self.series} for {self.arch} @ {self.timestamp}" |
92 | + |
93 | + @property |
94 | + def timestamp(self) -> str: |
95 | + """Formats the result's completion time as a string.""" |
96 | + return time.strftime("%d.%m.%y %H:%M:%S", self.time) |
97 | + |
98 | + @cached_property |
99 | + def log(self) -> str: |
100 | + """Returns log contents for results, downloading if necessary. |
101 | + |
102 | + Retrieves the log via the result url, handles decompression, and |
103 | + caches the results internally, so that subsequent calls don't |
104 | + re-download the data. |
105 | + |
106 | + On error, returns None and stores the error message in |
107 | + the Result.error_message property. |
108 | + |
109 | + :rtype: str |
110 | + :returns: Full text of the log file, or None on error. |
111 | + """ |
112 | + request = urllib.request.Request(self.url) |
113 | + request.add_header('Cache-Control', 'max-age=0') |
114 | + try: |
115 | + response = urllib.request.urlopen(request) |
116 | + except urllib.error.HTTPError as e: |
117 | + self.error_message = f"Failed to Download Test Log ⚪: {e}" |
118 | + return None |
119 | + |
120 | + result_gzip = response.read() |
121 | + try: |
122 | + return gzip.decompress(result_gzip).decode("utf-8", |
123 | + errors="replace") |
124 | + except UnicodeDecodeError: |
125 | + self.error_message = "Broken Test Log ⚪" |
126 | + return None |
127 | + |
128 | + # TODO: Merge triggers and get_triggers() |
129 | + @cached_property |
130 | + def triggers(self) -> List[str]: |
131 | + """Returns package/version parameters used to generate this Result. |
132 | + |
133 | + This returns the set of triggers used to create the Result, as |
134 | + recorded in the test log. Each trigger is a package/version |
135 | + pair corresponding to source packages to use from the proposed |
136 | + archive (instead of from the release archive). |
137 | + |
138 | + :rtype: list[str] |
139 | + :returns: List of package/version triggers. |
140 | + """ |
141 | + regex_triggers = re.compile(r'--env=ADT_TEST_TRIGGERS=(.*?) -- ') |
142 | + header_split = self.log.split(": @@@@@@@@@@@@@@@@@@@@", 1) |
143 | + if len(header_split) < 2: |
144 | + return [] |
145 | + m = re.search(regex_triggers, header_split[0]) |
146 | + if not m: |
147 | + return [] |
148 | + |
149 | + return m.group(1).strip("'").split() |
150 | + |
151 | + @lru_cache |
152 | + def get_triggers(self, name=None) -> Iterator[Trigger]: |
153 | + """Provides list of Triggers that were used to create this Result. |
154 | + |
155 | + This returns the set of Triggers used to create the Result, as |
156 | + recorded in the test log. Each trigger identifies a |
157 | + package/version pair corresponding to source packages to use |
158 | + from the proposed archive (instead of from the release archive). |
159 | + |
160 | + :param str name: If defined, only return triggers starting with this name. |
161 | + :rtype: Iterator[Trigger] |
162 | + :returns: Triggers used to generate this Result, if any, or an empty list |
163 | + """ |
164 | + if not self.triggers: |
165 | + return [] |
166 | + |
167 | + for t in self.triggers: |
168 | + package, version = t.split('/', 1) |
169 | + yield Trigger(package, version, arch=self.arch, series=self.series) |
170 | + |
171 | + |
172 | + @lru_cache |
173 | + def get_subtests(self, name=None) -> Iterator[Subtest]: |
174 | + """Provides list of Subtests that were run for this Result. |
175 | + |
176 | + :param str name: Only display subtests starting with this. |
177 | + :rtype: Iterator[Subtest] |
178 | + :returns: Subtests completed for this Result, or empty list. |
179 | + """ |
180 | + result_split = self.log.split("@@@@@@@@@@@@@@@@@@@@ summary", 1) |
181 | + if len(result_split) < 2: |
182 | + return [] |
183 | + |
184 | + result_sum = result_split[1] |
185 | + for line in re.findall("(.*PASS|.*SKIP|.*FAIL|.*BAD)", result_sum): |
186 | + if name and not line.startswith(name): |
187 | + continue |
188 | + yield Subtest(line) |
189 | + |
190 | + @cached_property |
191 | + def status(self) -> str: |
192 | + """Returns overall status of all subtests |
193 | + |
194 | + If the triggered run completed successfully, then the status will |
195 | + be either FAIL if any of the subtests failed, or PASS otherwise. |
196 | + |
197 | + If the run did not complete successfully, then a 'BAD' status |
198 | + will be returned, and the reason can be examined via the |
199 | + Result.error_message property. |
200 | + |
201 | + :rtype: str |
202 | + :returns: 'PASS', 'FAIL', or 'BAD' |
203 | + """ |
204 | + if self.error_message: |
205 | + return 'BAD' |
206 | + |
207 | + for subtest in self.get_subtests(): |
208 | + if subtest.status == 'FAIL': |
209 | + return 'FAIL' |
210 | + return 'PASS' |
211 | + |
212 | + @cached_property |
213 | + def status_icon(self) -> str: |
214 | + """Unicode symbol corresponding to test's overall status. |
215 | + |
216 | + :rtype: str |
217 | + :returns: Unicode symbol |
218 | + """ |
219 | + return Result.VALUES[self.status] |
220 | + |
221 | + |
222 | +def get_results(response, base_url, arches=None, sources=None) -> Iterator[Result]: |
223 | + """Returns iterator of Results from the base URL for given criteria |
224 | + |
225 | + Retrieves the autopkgtest results limited to the given architectures |
226 | + and source packages. If unspecified, returns all results. |
227 | + |
228 | + :param str base_url: URL for the autopkgtest results. |
229 | + :param list[str] arches: Architectures to include in results. |
230 | + :param list[str] sources: Source packages to include in results. |
231 | + :rtype: Iterator[Result] |
232 | + :returns: Iterable results, if any, or an empty list on error |
233 | + """ |
234 | + for line in response.read().split(b'\n'): |
235 | + if line == b'' or not line.endswith(b"log.gz"): |
236 | + continue |
237 | + result = line.decode("utf-8") |
238 | + series, arch, _, source, timestamp = result.split('/')[:5] |
239 | + if (arches and (arch not in arches)): |
240 | + continue |
241 | + if (sources and (source not in sources)): |
242 | + continue |
243 | + yield Result( |
244 | + url=base_url + result, |
245 | + time=time.strptime(timestamp[:-7], "%Y%m%d_%H%M%S"), |
246 | + series=series, |
247 | + arch=arch, |
248 | + source=source) |
249 | + |
250 | + |
251 | +if __name__ == "__main__": |
252 | + import os |
253 | + |
254 | + from ppa.io import open_url |
255 | + from ppa.constants import ARCHES_AUTOPKGTEST, URL_AUTOPKGTEST |
256 | + |
257 | + print("### Result class smoke test ###") |
258 | + |
259 | + timestamp = time.strptime('20030201_040506', "%Y%m%d_%H%M%S") |
260 | + result_1 = Result('url-here', timestamp, 'kinetic', 'amd64', 'my-package') |
261 | + print(repr(result_1)) |
262 | + print(result_1) |
263 | + print() |
264 | + |
265 | + data_dir = os.path.realpath(os.path.join(os.path.dirname(__file__), "../tests/data")) |
266 | + url = f"file://{data_dir}/results-six-s390x.log.gz" |
267 | + result_2 = Result(url, timestamp, 'kinetic', 'amd64', 'my-package') |
268 | + print("Log Head:") |
269 | + print("\n".join(result_2.log.splitlines()[0:4])) |
270 | + print() |
271 | + |
272 | + # TODO: Implement something that dumps the passing tests for given package from -proposed |
273 | + # TODO: Filter to items with only Pass, Not a regression, or No test results |
274 | + |
275 | + print("Loading live excuses data") |
276 | + base_results_fmt = f"{URL_AUTOPKGTEST}/results/autopkgtest-%s-%s-%s/" |
277 | + base_results_url = base_results_fmt % ('kinetic', 'bryce', 'nginx-merge-v1.22.0-1') |
278 | + url = f"{base_results_url}?format=plain" |
279 | + response = open_url(url) |
280 | + |
281 | + for result in get_results(response, base_results_url, arches=ARCHES_AUTOPKGTEST): |
282 | + print(f"* {result}") |
283 | + print(f" - Triggers: " + ', '.join([str(r) for r in result.get_triggers()])) |
284 | + |
285 | + for subtest in result.get_subtests(): |
286 | + print(f" - {subtest}") |
287 | + |
288 | + print() |
289 | diff --git a/ppa/subtest.py b/ppa/subtest.py |
290 | new file mode 100755 |
291 | index 0000000..c63dda6 |
292 | --- /dev/null |
293 | +++ b/ppa/subtest.py |
294 | @@ -0,0 +1,109 @@ |
295 | +#!/usr/bin/env python3 |
296 | +# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- |
297 | + |
298 | +# Copyright (C) 2022 Authors |
299 | +# |
300 | +# Released under GNU GPLv2 or later, read the file 'LICENSE.GPLv2+' for |
301 | +# more information. |
302 | +# |
303 | +# Authors: |
304 | +# Bryce Harrington <bryce@canonical.com> |
305 | + |
306 | +"""An individual DEP8 test run""" |
307 | + |
308 | +from functools import cached_property |
309 | +from typing import List, Iterator |
310 | + |
311 | + |
312 | +class Subtest: |
313 | + """ |
314 | + A triggered autopkgtest can invoke multiple DEP8 tests, such as running |
315 | + checks on dependencies, the software's testsuite, and integration tests. |
316 | + Each of these is considered a "Subtest" |
317 | + """ |
318 | + VALUES = { |
319 | + 'PASS': "🟩", |
320 | + 'SKIP': "🟧", |
321 | + 'FAIL': "🟥", |
322 | + 'BAD': "⛔", |
323 | + 'UNKNOWN': "⚪" |
324 | + } |
325 | + |
326 | + def __init__(self, line): |
327 | + """Initializes a new Subtext object. |
328 | + |
329 | + :param str line: The subtest result summary from a test log. |
330 | + """ |
331 | + if not line: |
332 | + raise ValueError("undefined line.") |
333 | + |
334 | + self._line = line |
335 | + |
336 | + def __repr__(self) -> str: |
337 | + """Machine-parsable unique representation of object. |
338 | + |
339 | + :rtype: str |
340 | + :returns: Official string representation of the object. |
341 | + """ |
342 | + return (f'{self.__class__.__name__}(' |
343 | + f'line={self._line!r})') |
344 | + |
345 | + def __str__(self) -> str: |
346 | + """Human-readable summary of the object. |
347 | + |
348 | + :rtype: str |
349 | + :returns: Printable summary of the object. |
350 | + """ |
351 | + return f"{self.status_icon} {self.desc}: {self.status}" |
352 | + |
353 | + @cached_property |
354 | + def desc(self) -> str: |
355 | + """The descriptive text for the given subtest. |
356 | + |
357 | + :rtype: str |
358 | + :returns: Descriptive text. |
359 | + """ |
360 | + d = self._line.rsplit(':', 1) |
361 | + if not d: |
362 | + return '' |
363 | + return d[0] |
364 | + |
365 | + @cached_property |
366 | + def status(self) -> str: |
367 | + """The success or failure of the given subtest. |
368 | + |
369 | + :rtype: str |
370 | + :returns: Status term in capitalized letters (PASS, FAIL, etc.) |
371 | + """ |
372 | + for k in Subtest.VALUES: |
373 | + if self._line.endswith(k): |
374 | + return k |
375 | + return 'UNKNOWN' |
376 | + |
377 | + @cached_property |
378 | + def status_icon(self) -> str: |
379 | + """Unicode symbol corresponding to subtest's status. |
380 | + |
381 | + :rtype: str |
382 | + :returns: Single unicode character matching the status. |
383 | + """ |
384 | + return Subtest.VALUES[self.status] |
385 | + |
386 | + |
387 | +if __name__ == "__main__": |
388 | + print("### Subtest class smoke test ###") |
389 | + print() |
390 | + |
391 | + print("Valid cases") |
392 | + print("-----------") |
393 | + print(Subtest('subtest a: UNKNOWN')) |
394 | + print(Subtest('subtest b: PASS')) |
395 | + print(Subtest('subtest c: FAIL')) |
396 | + print(Subtest('subtest d: SKIP')) |
397 | + print(Subtest('subtest e: BAD')) |
398 | + print() |
399 | + |
400 | + print("Invalid cases") |
401 | + print("-------------") |
402 | + print(Subtest('invalid subtest: invalid')) |
403 | + print(Subtest('bAd subtest: bAd')) |
404 | diff --git a/ppa/trigger.py b/ppa/trigger.py |
405 | new file mode 100755 |
406 | index 0000000..79276da |
407 | --- /dev/null |
408 | +++ b/ppa/trigger.py |
409 | @@ -0,0 +1,80 @@ |
410 | +#!/usr/bin/env python3 |
411 | +# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- |
412 | + |
413 | +# Copyright (C) 2022 Authors |
414 | +# |
415 | +# Released under GNU GPLv2 or later, read the file 'LICENSE.GPLv2+' for |
416 | +# more information. |
417 | +# |
418 | +# Authors: |
419 | +# Bryce Harrington <bryce@canonical.com> |
420 | + |
421 | +"""A directive to run a DEP8 test against a source package""" |
422 | + |
423 | +from functools import cached_property |
424 | + |
425 | +from .constants import URL_AUTOPKGTEST |
426 | + |
427 | + |
428 | +class Trigger: |
429 | + """ |
430 | + A trigger indicates a source package that should be installed from a |
431 | + given series (generally to a specific version) when running |
432 | + autopkgtests for a Job. |
433 | + |
434 | + A Job can have multiple Triggers, each against a different source |
435 | + package and/or architectures, but all such Triggers must be against |
436 | + the same series as the Job itself. |
437 | + """ |
438 | + def __init__(self, package, version, arch, series): |
439 | + """Initializes a new Trigger for a given package and version. |
440 | + |
441 | + :param str package: The source package name. |
442 | + :param str version: The version of the source package to install. |
443 | + :param str arch: The architecture for the trigger. |
444 | + :param str series: The distro release series codename. |
445 | + """ |
446 | + self.package = package |
447 | + self.version = version |
448 | + self.arch = arch |
449 | + self.series = series |
450 | + |
451 | + def __repr__(self) -> str: |
452 | + """Machine-parsable unique representation of object. |
453 | + |
454 | + :rtype: str |
455 | + :returns: Official string representation of the object. |
456 | + """ |
457 | + return (f'{self.__class__.__name__}(' |
458 | + f'package={self.package!r}, version={self.version!r}, ' |
459 | + f'arch={self.arch!r}, series={self.series!r})') |
460 | + |
461 | + def __str__(self) -> str: |
462 | + """Human-readable summary of the object. |
463 | + |
464 | + :rtype: str |
465 | + :returns: Printable summary of the object. |
466 | + """ |
467 | + return f"{self.package}/{self.version}" |
468 | + |
469 | + @cached_property |
470 | + def autopkgtest_url(self) -> str: |
471 | + """Renders the trigger as a URL to the job history. |
472 | + |
473 | + :rtype: str |
474 | + :returns: tbd |
475 | + """ |
476 | + if self.package.startswith('lib'): |
477 | + prefix = self.package[0:4] |
478 | + else: |
479 | + prefix = self.package[0] |
480 | + pkg_str = f"{prefix}/{self.package}" |
481 | + return f"{URL_AUTOPKGTEST}/packages/{pkg_str}/{self.series}/{self.arch}" |
482 | + |
483 | + |
484 | +if __name__ == "__main__": |
485 | + print("### Trigger class smoke test ###") |
486 | + |
487 | + trigger = Trigger('my-package', '1.2.3', 'amd64', 'kinetic') |
488 | + print(trigger) |
489 | + print(trigger.autopkgtest_url) |
490 | diff --git a/tests/data/results-six-s390x.log.gz b/tests/data/results-six-s390x.log.gz |
491 | new file mode 100644 |
492 | index 0000000..70d1a80 |
493 | Binary files /dev/null and b/tests/data/results-six-s390x.log.gz differ |
494 | diff --git a/tests/test_io.py b/tests/test_io.py |
495 | index fce6611..57f8737 100644 |
496 | --- a/tests/test_io.py |
497 | +++ b/tests/test_io.py |
498 | @@ -13,10 +13,9 @@ |
499 | import os |
500 | import sys |
501 | import urllib |
502 | -import pytest |
503 | |
504 | sys.path.insert(0, os.path.realpath( |
505 | - os.path.join(os.path.dirname(__file__), ".."))) |
506 | + os.path.join(os.path.dirname(__file__), ".."))) |
507 | |
508 | from ppa.io import open_url |
509 | |
510 | @@ -28,5 +27,5 @@ def test_open_url(tmp_path): |
511 | |
512 | request = open_url(f"file://{f}") |
513 | assert request |
514 | - assert type(request) == urllib.response.addinfourl |
515 | + assert isinstance(request, urllib.response.addinfourl) |
516 | assert request.read().decode() == 'abcde' |
517 | diff --git a/tests/test_job.py b/tests/test_job.py |
518 | index d9a9967..44491c2 100644 |
519 | --- a/tests/test_job.py |
520 | +++ b/tests/test_job.py |
521 | @@ -14,16 +14,18 @@ import os |
522 | import sys |
523 | |
524 | sys.path.insert(0, os.path.realpath( |
525 | - os.path.join(os.path.dirname(__file__), ".."))) |
526 | + os.path.join(os.path.dirname(__file__), ".."))) |
527 | |
528 | from ppa.job import Job, get_running, get_waiting |
529 | |
530 | |
531 | class ResponseMock: |
532 | + """Synthetic response object""" |
533 | def __init__(self, text): |
534 | self._text = text.encode('utf-8') |
535 | |
536 | def read(self): |
537 | + """Simply returns the exact text provided in initializer.""" |
538 | return self._text |
539 | |
540 | |
541 | @@ -34,6 +36,7 @@ def test_object(): |
542 | |
543 | |
544 | def test_init(): |
545 | + """Checks the initialization of a Job object.""" |
546 | job = Job(0, 'a', 'b', 'c', 'd') |
547 | assert job.number == 0 |
548 | assert job.submit_time == 'a' |
549 | @@ -71,9 +74,10 @@ def test_request_url(): |
550 | jobinfo = { |
551 | 'triggers': ['t/1'], |
552 | 'ppas': ['ppa:a/b', 'ppa:c/d'] |
553 | - } |
554 | + } |
555 | job = Job(0, 'a', 'b', 'c', 'd', jobinfo['triggers'], jobinfo['ppas']) |
556 | - assert job.request_url == "https://autopkgtest.ubuntu.com/request.cgi?release=c&arch=d&package=b&trigger=t/1" |
557 | + assert job.request_url.startswith("https://autopkgtest.ubuntu.com/request.cgi") |
558 | + assert job.request_url.endswith("?release=c&arch=d&package=b&trigger=t/1") |
559 | |
560 | |
561 | def test_get_running(): |
562 | diff --git a/tests/test_result.py b/tests/test_result.py |
563 | new file mode 100644 |
564 | index 0000000..836bbd1 |
565 | --- /dev/null |
566 | +++ b/tests/test_result.py |
567 | @@ -0,0 +1,66 @@ |
568 | +#!/usr/bin/env python3 |
569 | +# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- |
570 | + |
571 | +# Author: Bryce Harrington <bryce@canonical.com> |
572 | +# |
573 | +# Copyright (C) 2022 Bryce W. Harrington |
574 | +# |
575 | +# Released under GNU GPLv2 or later, read the file 'LICENSE.GPLv2+' for |
576 | +# more information. |
577 | + |
578 | +"""Results class tests""" |
579 | + |
580 | +import os |
581 | +import sys |
582 | +import time |
583 | + |
584 | +import gzip |
585 | + |
586 | +sys.path.insert(0, os.path.realpath( |
587 | + os.path.join(os.path.dirname(__file__), ".."))) |
588 | + |
589 | +from ppa.result import Result |
590 | + |
591 | + |
592 | +def test_object(): |
593 | + """Checks that Result objects can be instantiated.""" |
594 | + result = Result('url', None, 'b', 'c', 'd') |
595 | + assert result |
596 | + |
597 | + |
598 | +def test_repr(): |
599 | + """Checks Result object representation.""" |
600 | + result = Result('url', None, 'b', 'c', 'd') |
601 | + # TODO: Should this include the full set of args? |
602 | + assert repr(result) == "Result(url='url')" |
603 | + |
604 | + |
605 | +def test_str(): |
606 | + """Checks Result object textual presentation.""" |
607 | + timestamp = time.strptime('20030201_040506', "%Y%m%d_%H%M%S") |
608 | + result = Result('url', timestamp, 'b', 'c', 'd') |
609 | + assert f"{result}" == 'd on b for c @ 01.02.03 04:05:06' |
610 | + |
611 | + |
612 | +def test_timestamp(): |
613 | + """Checks Result object formats the result's time correctly.""" |
614 | + timestamp = time.strptime('20030201_040506', "%Y%m%d_%H%M%S") |
615 | + result = Result('url', timestamp, 'b', 'c', 'd') |
616 | + assert f"{result.timestamp}" == '01.02.03 04:05:06' |
617 | + |
618 | + |
619 | +def test_log(tmp_path): |
620 | + """Checks that the log content of a Result is available.""" |
621 | + f = tmp_path / "result.log.gz" |
622 | + compressed_text = gzip.compress(bytes('abcde', 'utf-8')) |
623 | + f.write_bytes(compressed_text) |
624 | + |
625 | + result = Result(f"file://{f}", None, None, None, None) |
626 | + assert result.log == "abcde" |
627 | + |
628 | + |
629 | +def test_triggers(): |
630 | + """Checks that autopkgtest triggers can be extracted from test result logs.""" |
631 | + data_dir = "/home/bryce/src/PpaDevTools/ppa-dev-tools-result-class/tests/data" |
632 | + result = Result(f"file://{data_dir}/results-six-s390x.log.gz", None, None, None, None) |
633 | + assert result.triggers == ['pygobject/3.42.2-2', 'six/1.16.0-4'] |
634 | diff --git a/tests/test_subtest.py b/tests/test_subtest.py |
635 | new file mode 100644 |
636 | index 0000000..6ca35af |
637 | --- /dev/null |
638 | +++ b/tests/test_subtest.py |
639 | @@ -0,0 +1,86 @@ |
640 | +#!/usr/bin/env python3 |
641 | +# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- |
642 | + |
643 | +# Author: Bryce Harrington <bryce@canonical.com> |
644 | +# |
645 | +# Copyright (C) 2022 Bryce W. Harrington |
646 | +# |
647 | +# Released under GNU GPLv2 or later, read the file 'LICENSE.GPLv2+' for |
648 | +# more information. |
649 | + |
650 | +"""Subtest class tests""" |
651 | + |
652 | +import os |
653 | +import sys |
654 | + |
655 | +import pytest |
656 | + |
657 | +sys.path.insert(0, os.path.realpath( |
658 | + os.path.join(os.path.dirname(__file__), ".."))) |
659 | + |
660 | +from ppa.subtest import Subtest |
661 | + |
662 | + |
663 | +def test_object(): |
664 | + """Checks that Subtest objects can be instantiated.""" |
665 | + subtest = Subtest('a: UNKNOWN') |
666 | + # TODO: If no ':' in description, should throw exception? |
667 | + # TODO: Or add a function that parses lines and returns subtests? |
668 | + assert subtest |
669 | + |
670 | + |
671 | +def test_repr(): |
672 | + """Checks Subtest object representation.""" |
673 | + subtest = Subtest('a: PASS') |
674 | + assert repr(subtest) == "Subtest(line='a: PASS')" |
675 | + |
676 | + |
677 | +def test_str(): |
678 | + """Checks Subtest object textual presentation.""" |
679 | + subtest = Subtest('a: PASS') |
680 | + assert f"{subtest}" == '🟩 a: PASS' |
681 | + |
682 | + |
683 | +def test_desc(): |
684 | + """Checks Subtest description is parsed correctly.""" |
685 | + subtest = Subtest('a: PASS') |
686 | + assert subtest.desc == 'a' |
687 | + |
688 | + subtest = Subtest('a:b : c:d: PASS') |
689 | + assert subtest.desc == 'a:b : c:d' |
690 | + |
691 | + |
692 | +@pytest.mark.parametrize('line, status', [ |
693 | + ('a: PASS', 'PASS'), |
694 | + ('b: SKIP', 'SKIP'), |
695 | + ('c: FAIL', 'FAIL'), |
696 | + ('d: BAD', 'BAD'), |
697 | + ('e: UNKNOWN', 'UNKNOWN'), |
698 | + ('f: invalid', 'UNKNOWN'), |
699 | + ('f: bAd', 'UNKNOWN'), |
700 | +]) |
701 | +def test_status(line, status): |
702 | + """Checks Subtest status is parsed correctly.""" |
703 | + subtest = Subtest(line) |
704 | + assert subtest.status == status |
705 | + |
706 | + subtest = Subtest('a: PASS') |
707 | + |
708 | + |
709 | +@pytest.mark.parametrize('line, icon', [ |
710 | + ('a: PASS', "🟩"), |
711 | + ('b: SKIP', "🟧"), |
712 | + ('c: FAIL', "🟥"), |
713 | + ('d: BAD', "⛔"), |
714 | + ('e: UNKNOWN', "⚪"), |
715 | + ('f: invalid', "⚪"), |
716 | + ('f: bAd', "⚪"), |
717 | +]) |
718 | +def test_status_icon(line, icon): |
719 | + """Checks Subtest provides correct icon for status. |
720 | + |
721 | + :param str line: Subtest status line to be parsed. |
722 | + :param str icon: Resulting status icon that should be returned. |
723 | + """ |
724 | + subtest = Subtest(line) |
725 | + assert subtest.status_icon == icon |
726 | diff --git a/tests/test_trigger.py b/tests/test_trigger.py |
727 | new file mode 100644 |
728 | index 0000000..c6b6a6b |
729 | --- /dev/null |
730 | +++ b/tests/test_trigger.py |
731 | @@ -0,0 +1,55 @@ |
732 | +#!/usr/bin/env python3 |
733 | +# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- |
734 | + |
735 | +# Author: Bryce Harrington <bryce@canonical.com> |
736 | +# |
737 | +# Copyright (C) 2022 Bryce W. Harrington |
738 | +# |
739 | +# Released under GNU GPLv2 or later, read the file 'LICENSE.GPLv2+' for |
740 | +# more information. |
741 | + |
742 | +"""Trigger class tests""" |
743 | + |
744 | +import os |
745 | +import sys |
746 | + |
747 | +sys.path.insert(0, os.path.realpath( |
748 | + os.path.join(os.path.dirname(__file__), ".."))) |
749 | + |
750 | +from ppa.trigger import Trigger |
751 | + |
752 | + |
753 | +def test_object(): |
754 | + """Checks that Trigger objects can be instantiated.""" |
755 | + trigger = Trigger('a', 'b', 'c', 'd') |
756 | + assert trigger |
757 | + |
758 | + |
759 | +def test_repr(): |
760 | + """Checks Trigger object representation.""" |
761 | + trigger = Trigger('a', 'b', 'c', 'd') |
762 | + assert repr(trigger) == "Trigger(package='a', version='b', arch='c', series='d')" |
763 | + |
764 | + |
765 | +def test_str(): |
766 | + """Checks Trigger object textual presentation.""" |
767 | + trigger = Trigger('a', 'b', 'c', 'd') |
768 | + assert f"{trigger}" == 'a/b' |
769 | + |
770 | + trigger = Trigger('dovecot', '1:2.3.19.1+dfsg1-2ubuntu2', 'i386', 'kinetic') |
771 | + assert f"{trigger}" == 'dovecot/1:2.3.19.1+dfsg1-2ubuntu2' |
772 | + |
773 | + |
774 | +def test_autopkgtest_url(): |
775 | + """Checks that Trigger objects generate valid autopkgtest urls""" |
776 | + trigger = Trigger('a', 'b', 'c', 'd') |
777 | + expected = "https://autopkgtest.ubuntu.com/packages/a/a/d/c" |
778 | + assert trigger.autopkgtest_url == expected |
779 | + |
780 | + trigger = Trigger('apache2', '2.4', 'amd64', 'kinetic') |
781 | + expected = "https://autopkgtest.ubuntu.com/packages/a/apache2/kinetic/amd64" |
782 | + assert trigger.autopkgtest_url == expected |
783 | + |
784 | + trigger = Trigger('libwebsockets', '4.1.6-3', 'armhf', 'jammy') |
785 | + expected = "https://autopkgtest.ubuntu.com/packages/libw/libwebsockets/jammy/armhf" |
786 | + assert trigger.autopkgtest_url == expected |
Thanks, Bryce. LGTM!
I left a few inline comments, including a question, suggestion on a changed test.