Merge lp:~jibel/britney/fix_missing_results into lp:~ubuntu-release/britney/britney2-ubuntu
- fix_missing_results
- Merge into britney2-ubuntu
Status: | Merged |
---|---|
Merged at revision: | 398 |
Proposed branch: | lp:~jibel/britney/fix_missing_results |
Merge into: | lp:~ubuntu-release/britney/britney2-ubuntu |
Diff against target: |
536 lines (+244/-73) 3 files modified
autopkgtest.py (+56/-32) britney.py (+6/-5) tests/test_autopkgtest.py (+182/-36) |
To merge this branch: | bzr merge lp:~jibel/britney/fix_missing_results |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Colin Watson (community) | Approve | ||
Martin Pitt (community) | Approve | ||
Review via email: mp+218467@code.launchpad.net |
Commit message
Description of the change
This branch fix a case of "vanishing" results in excuses.
Only the last result in history for a given package was considered, results being sorted in alphabetic order, a failed test could be hidden by another test that passed, and the package promoted while it should have been blocked.
For example if we had the following result in history:
udisk2 1 PASS udisks2 1
An upload of systemd will trigger a test of udisk2, if we suppose that this test fails, it will generate the following list of results:
udisk2 1 FAIL systemd 2
udisk2 1 PASS udisk2 1
Which means that udisk2 can be promoted because it pass, and systemd must be blocked because it introduced a regression in udisk2.
With the bug only the last cause in the list is considered and the result for udisk2/systemd is ignored.
This MP also:
- Adds tests to cover the bug mentioned above, and update existing tests for the new states "always fails/regression"
- makes the distinction between tests that are always failing and consider them as valid candidates and tests that regress, in which case the package is block.
- Add colouring to make excuses more readable for long list of autopkgtests
- Points jenkins URL directly lastBuild to save some clicks
Martin Pitt (pitti) wrote : | # |
autopkgtest.py
==============
- logging.warning(
- "Invalid line format: '%s', skipped" % line)
+ print("W: Invalid line format: '%s', skipped" % line)
This indeed should be fixed. However, for consistency this should use self.__log(..., type='W') instead of print.
+ if not trigsrc in self.pkglist[
+ self.pkglist[
+ self.pkglist[
+ status))
This is a common pattern in Python which is much more succinct with a dict's setdefault() method. I. e.
It would be helpful to add a docstring to read() to say what it reads (the results file), what its format is, and what the structure of the generated self.pkgcauses and self.pkglist are.
Otherwise the new logic seems fine to me (thanks to JB for additional explanations on IRC).
britney.py
==========
+ adt_label = status
+ if status in ADT_EXCUSES_LABELS:
+ adt_label = ADT_EXCUSES_
Do we realistically expect a status which isn't in the map? If not (and we want to get pointed to that error), I'd suggest to simply use the dict lookup. If we want to allow that possibility, I suggest this for simplification:
adt_label = ADT_EXCUSES_
LGTM otherwise.
- 414. By Jean-Baptiste Lallement
-
* tests/test_
autopkgtest. py:
- Added docstrings
- Replaced format by %
- Import ADT_EXCUSES_LABELS from autopkgtest.py instead of redefining it
- Renamed test to avoid name conflict with autopkgtest.py from britney
- Fixed some formatting - 415. By Jean-Baptiste Lallement
-
autopkgtest.py: Document method read() and code simplification
britney.py: code simplification
Jean-Baptiste Lallement (jibel) wrote : | # |
> Code review for the new tests:
>
> * Checking the precise HTML formatting and colors in the tests seems a bit
> excessive to me, but I don't mind it much. But if you want to keep it, please
> don't duplicate the definition of ADT_EXCUSES_LABELS in the test but import it
> from autopkgtest (note, this might require renaming tests/autopkgte
> tests/test_
I renamed tests/autopkgte
>
> * __merge_records() could do with a docstring, as it's not quite clear what
> this does and why it's needed in a test. That's the sort of logic I'd expect
> in the actual code, but in the test suite the fake results should be pre-
> formatted correctly? Also, is it actually justified to assume any particular
> ordering in history.txt, or should autopkgtest.py not assume any and just
> order it by itself?
The sorting logic should indeed be in the fake britney, but it is more convenient to have actual python code in the test and make the fake adt-britney just return static data.
Results are sorted by version to have latest results last in the list. There is no other justification than to the ordering than keeping results for a given package grouped. I should revisit it in adt-britney.
>
> def test_request_
> '''Requests a test for an installable package, test fail'''
>
> I think it would make sense to split this in two: One should be the code as it
> is right now (i. e. with empty history), and be called
> "test_request_
> just one (or perhaps two) FAILs, which is called
> "test_request_
> they both should count as ALWAYSFAIL, but I think it's still important to see
> that both cases behave as expected.
I renamed the test to clarify what this test does (first result fail) and the second case (test failed and previous test failed too) is already covered by test_history_
>
> + def test_request_
> + '''Requests a test for an installable package, test fail, is a
> + regression.
>
> Please no multi-line short docstrings, this causes truncated descriptions when
> running with -v and is also against PEP-8/looks ugly.
Fixed.
>
>
> Some really small nitpicks:
>
> + ''' % {'py': sys.executable, 'path': self.data.path,
> 'rq':
>
> Please move the 'rq': to the next line so that key and value are together,
> for better readability.
>
Fixed
> + '<li>autopkgtest for green 1.1~beta:
> {}'.format(
>
> We use %s macros (or similar) and the % operator instead of {} and .format()
> in other places, so maybe this could be made consistent?
>
Fixed
> + In this case results for A 1didn't appear in the list of results
>
> missing space after '1'. Also, excess empty lines after the docstring.
>
Fixed
> I'll do the code review for the actual britney.py and autopkgtest.py code in a
> separate comment.
Thanks for your review
Jean-Baptiste Lallement (jibel) wrote : | # |
> autopkgtest.py
> ==============
>
> - logging.warning(
> - "Invalid line format: '%s', skipped" % line)
> + print("W: Invalid line format: '%s', skipped" % line)
>
> This indeed should be fixed. However, for consistency this should use
> self.__log(..., type='W') instead of print.
>
Actually __log() is a method of britney and used in britney.py. In autopkgtest.py only print() is used.
>
> + if not trigsrc in self.pkglist[
> + self.pkglist[
> + self.pkglist[
> + status))
>
> This is a common pattern in Python which is much more succinct with a dict's
> setdefault() method. I. e.
Fixed.
>
> self.pkglist[
> []).append(
> (trigver, status))
>
> It would be helpful to add a docstring to read() to say what it reads (the
> results file), what its format is, and what the structure of the generated
> self.pkgcauses and self.pkglist are.
Added documentation.
>
> Otherwise the new logic seems fine to me (thanks to JB for additional
> explanations on IRC).
>
> britney.py
> ==========
>
> + adt_label = status
> + if status in ADT_EXCUSES_LABELS:
> + adt_label = ADT_EXCUSES_
>
Fixed.
> Do we realistically expect a status which isn't in the map? If not (and we
> want to get pointed to that error), I'd suggest to simply use the dict lookup.
> If we want to allow that possibility, I suggest this for simplification:
>
> adt_label = ADT_EXCUSES_
>
because adt-britney and britney are 2 separate projects, it is possible, while unlikely, that we add new statuses in adt-britney that are not in britney and make britney crash if the status is unknown.
>
> LGTM otherwise.
Martin Pitt (pitti) wrote : | # |
Thanks Jean-Baptiste! Looks good to me now, passing on to release team / Colin.
- 416. By Jean-Baptiste Lallement
-
Merged trunk
Colin Watson (cjwatson) : | # |
Preview Diff
1 | === modified file 'autopkgtest.py' |
2 | --- autopkgtest.py 2013-11-15 09:20:54 +0000 |
3 | +++ autopkgtest.py 2014-05-12 13:01:57 +0000 |
4 | @@ -19,18 +19,24 @@ |
5 | |
6 | from collections import defaultdict |
7 | from contextlib import closing |
8 | -import logging |
9 | import os |
10 | import subprocess |
11 | import tempfile |
12 | from textwrap import dedent |
13 | import time |
14 | - |
15 | import apt_pkg |
16 | |
17 | |
18 | adt_britney = os.path.expanduser("~/auto-package-testing/jenkins/adt-britney") |
19 | |
20 | +ADT_PASS = ["PASS", "ALWAYSFAIL"] |
21 | +ADT_EXCUSES_LABELS = { |
22 | + "PASS": '<span style="background:#87d96c">Pass</span>', |
23 | + "ALWAYSFAIL": '<span style="background:#e5c545">Always failed</span>', |
24 | + "REGRESSION": '<span style="background:#ff6666">Regression</span>', |
25 | + "RUNNING": '<span style="background:#99ddff">Test in progress</span>', |
26 | +} |
27 | + |
28 | |
29 | class AutoPackageTest(object): |
30 | """autopkgtest integration |
31 | @@ -62,7 +68,7 @@ |
32 | components: main restricted universe multiverse |
33 | rsync_host: rsync://tachash.ubuntu-ci/adt/ |
34 | datadir: ~/proposed-migration/autopkgtest/data""" % |
35 | - (self.series, self.series, home)), file=rc_file) |
36 | + (self.series, self.series, home)), file=rc_file) |
37 | |
38 | @property |
39 | def _request_path(self): |
40 | @@ -85,38 +91,39 @@ |
41 | continue |
42 | linebits = line.split() |
43 | if len(linebits) < 2: |
44 | - logging.warning( |
45 | - "Invalid line format: '%s', skipped" % line) |
46 | + print("W: Invalid line format: '%s', skipped" % line) |
47 | continue |
48 | yield linebits |
49 | |
50 | def read(self): |
51 | + '''Loads a list of results |
52 | + |
53 | + This function loads a list of results returned by __parse() and builds |
54 | + 2 lists: |
55 | + - a list of source package/version with all the causes that |
56 | + triggered a test and the result of the test for this trigger. |
57 | + - a list of packages/version that triggered a test with the source |
58 | + package/version and result triggered by this package. |
59 | + These lists will be used in result() called from britney.py to generate |
60 | + excuses and now which uploads passed, caused regression or which tests |
61 | + have always been failing |
62 | + ''' |
63 | self.pkglist = defaultdict(dict) |
64 | self.pkgcauses = defaultdict(lambda: defaultdict(list)) |
65 | for linebits in self._parse(self._result_path): |
66 | - src = linebits.pop(0) |
67 | - ver = linebits.pop(0) |
68 | - self.pkglist[src][ver] = { |
69 | - "status": "NEW", |
70 | - "causes": {}, |
71 | + (src, ver, status) = linebits[:3] |
72 | + |
73 | + if not (src in self.pkglist and ver in self.pkglist[src]): |
74 | + self.pkglist[src][ver] = { |
75 | + "status": status, |
76 | + "causes": {} |
77 | } |
78 | - try: |
79 | - status = linebits.pop(0).upper() |
80 | - self.pkglist[src][ver]["status"] = status |
81 | - while True: |
82 | - trigsrc = linebits.pop(0) |
83 | - trigver = linebits.pop(0) |
84 | - self.pkglist[src][ver]["causes"][trigsrc] = trigver |
85 | - except IndexError: |
86 | - # End of the list |
87 | - pass |
88 | - for src in self.pkglist: |
89 | - all_vers = sorted(self.pkglist[src], cmp=apt_pkg.version_compare) |
90 | - for ver in self.pkglist[src]: |
91 | - status = self.pkglist[src][ver]["status"] |
92 | - for trigsrc, trigver in \ |
93 | - self.pkglist[src][ver]["causes"].items(): |
94 | - self.pkgcauses[trigsrc][trigver].append((status, src, ver)) |
95 | + |
96 | + i = iter(linebits[3:]) |
97 | + for trigsrc, trigver in zip(i, i): |
98 | + self.pkglist[src][ver]['causes'].setdefault( |
99 | + trigsrc, []).append((trigver, status)) |
100 | + self.pkgcauses[trigsrc][trigver].append((status, src, ver)) |
101 | |
102 | def _adt_britney(self, *args): |
103 | command = [ |
104 | @@ -197,12 +204,29 @@ |
105 | self.read() |
106 | if self.britney.options.verbose: |
107 | for src in sorted(self.pkglist): |
108 | - for ver in self.pkglist[src]: |
109 | - print("I: [%s] - Collected autopkgtest status for %s_%s: " |
110 | - "%s" % |
111 | - (time.asctime(), src, ver, |
112 | - self.pkglist[src][ver]["status"])) |
113 | + for ver in sorted(self.pkglist[src], |
114 | + cmp=apt_pkg.version_compare): |
115 | + for trigsrc in sorted(self.pkglist[src][ver]['causes']): |
116 | + for trigver, status \ |
117 | + in self.pkglist[src][ver]['causes'][trigsrc]: |
118 | + print("I: [%s] - Collected autopkgtest status " |
119 | + "for %s_%s/%s_%s: " "%s" % ( |
120 | + time.asctime(), src, ver, trigsrc, |
121 | + trigver, status)) |
122 | |
123 | def results(self, trigsrc, trigver): |
124 | for status, src, ver in self.pkgcauses[trigsrc][trigver]: |
125 | + # Check for regresssion |
126 | + if status == 'FAIL': |
127 | + passed_once = False |
128 | + for ver in self.pkglist[src]: |
129 | + for trigsrc in self.pkglist[src][ver]['causes']: |
130 | + for trigver, status \ |
131 | + in self.pkglist[src][ver]['causes'][trigsrc]: |
132 | + if status == 'PASS': |
133 | + passed_once = True |
134 | + if not passed_once: |
135 | + status = 'ALWAYSFAIL' |
136 | + else: |
137 | + status = 'REGRESSION' |
138 | yield status, src, ver |
139 | |
140 | === modified file 'britney.py' |
141 | --- britney.py 2014-03-05 16:14:48 +0000 |
142 | +++ britney.py 2014-05-12 13:01:57 +0000 |
143 | @@ -222,7 +222,7 @@ |
144 | from consts import (VERSION, SECTION, BINARIES, MAINTAINER, FAKESRC, |
145 | SOURCE, SOURCEVER, ARCHITECTURE, DEPENDS, CONFLICTS, |
146 | PROVIDES, RDEPENDS, RCONFLICTS, MULTIARCH) |
147 | -from autopkgtest import AutoPackageTest |
148 | +from autopkgtest import AutoPackageTest, ADT_PASS, ADT_EXCUSES_LABELS |
149 | |
150 | __author__ = 'Fabio Tranchitella and the Debian Release Team' |
151 | __version__ = '2.0' |
152 | @@ -1756,18 +1756,19 @@ |
153 | adtpass = True |
154 | for status, adtsrc, adtver in autopkgtest.results( |
155 | e.name, e.ver[1]): |
156 | - public_url = "%s/%s-adt-%s/" % ( |
157 | + public_url = "%s/%s-adt-%s/lastBuild" % ( |
158 | jenkins_public, self.options.adt_series, |
159 | adtsrc.replace("+", "-")) |
160 | - private_url = "%s/%s-adt-%s/" % ( |
161 | + private_url = "%s/%s-adt-%s/lastBuild" % ( |
162 | jenkins_private, self.options.adt_series, |
163 | adtsrc.replace("+", "-")) |
164 | + adt_label = ADT_EXCUSES_LABELS.get(status, status) |
165 | e.addhtml( |
166 | "autopkgtest for %s %s: %s (Jenkins: " |
167 | "<a href=\"%s\">public</a>, " |
168 | "<a href=\"%s\">private</a>)" % |
169 | - (adtsrc, adtver, status, public_url, private_url)) |
170 | - if status != "PASS": |
171 | + (adtsrc, adtver, adt_label, public_url, private_url)) |
172 | + if status not in ADT_PASS: |
173 | hints = self.hints.search( |
174 | 'force-badtest', package=adtsrc) |
175 | hints.extend( |
176 | |
177 | === renamed file 'tests/autopkgtest.py' => 'tests/test_autopkgtest.py' |
178 | --- tests/autopkgtest.py 2014-05-12 12:04:55 +0000 |
179 | +++ tests/test_autopkgtest.py 2014-05-12 13:01:57 +0000 |
180 | @@ -12,7 +12,10 @@ |
181 | import sys |
182 | import subprocess |
183 | import unittest |
184 | +import apt_pkg |
185 | +import operator |
186 | |
187 | +apt_pkg.init() |
188 | architectures = ['amd64', 'arm64', 'armhf', 'i386', 'powerpc', 'ppc64el'] |
189 | |
190 | my_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) |
191 | @@ -20,6 +23,9 @@ |
192 | NOT_CONSIDERED = False |
193 | VALID_CANDIDATE = True |
194 | |
195 | +sys.path.insert(0, my_dir) |
196 | +from autopkgtest import ADT_EXCUSES_LABELS |
197 | + |
198 | |
199 | class TestData: |
200 | def __init__(self): |
201 | @@ -157,7 +163,29 @@ |
202 | def tearDown(self): |
203 | del self.data |
204 | |
205 | - def make_adt_britney(self, request): |
206 | + def __merge_records(self, results, history=""): |
207 | + '''Merges a list of results with records in history. |
208 | + |
209 | + This function merges results from a collect with records already in |
210 | + history and sort records by version/name of causes and version/name of |
211 | + source packages with tests. This should be done in the fake |
212 | + adt-britney but it is more convenient to just pass a static list of |
213 | + records and make adt-britney just return this list. |
214 | + ''' |
215 | + |
216 | + if history is None: |
217 | + history = "" |
218 | + records = [x.split() for x in (results.strip() + '\n' + |
219 | + history.strip()).split('\n') if x] |
220 | + |
221 | + records.sort(cmp=apt_pkg.version_compare, key=operator.itemgetter(4)) |
222 | + records.sort(key=operator.itemgetter(3)) |
223 | + records.sort(cmp=apt_pkg.version_compare, key=operator.itemgetter(1)) |
224 | + records.sort() |
225 | + |
226 | + return "\n".join([' '.join(x) for x in records]) |
227 | + |
228 | + def make_adt_britney(self, request, history=""): |
229 | with open(self.adt_britney, 'w') as f: |
230 | f.write('''#!%(py)s |
231 | import argparse, shutil,sys |
232 | @@ -175,7 +203,7 @@ |
233 | |
234 | def collect(): |
235 | with open(args.output, 'w') as f: |
236 | - f.write("""%(rq)s""") |
237 | + f.write("""%(res)s""") |
238 | |
239 | p = argparse.ArgumentParser() |
240 | p.add_argument('-c', '--config') |
241 | @@ -202,7 +230,9 @@ |
242 | |
243 | args = p.parse_args() |
244 | args.func() |
245 | -''' % {'py': sys.executable, 'path': self.data.path, 'rq': request}) |
246 | + ''' % {'py': sys.executable, 'path': self.data.path, |
247 | + 'rq': request, |
248 | + 'res': self.__merge_records(request, history)}) |
249 | |
250 | def run_britney(self, args=[]): |
251 | '''Run britney. |
252 | @@ -245,9 +275,19 @@ |
253 | 'green 1.1~beta RUNNING green 1.1~beta\n', |
254 | NOT_CONSIDERED, |
255 | [r'\bgreen\b.*>1</a> to .*>1.1~beta<', |
256 | - '<li>autopkgtest for green 1.1~beta: RUNNING']) |
257 | - |
258 | - def test_request_for_installable_fail(self): |
259 | + '<li>autopkgtest for green 1.1~beta: %s' % ADT_EXCUSES_LABELS['RUNNING']]) |
260 | + |
261 | + def test_request_for_installable_first_fail(self): |
262 | + '''Requests a test for an installable package. No history and first result is a failure''' |
263 | + |
264 | + self.do_test( |
265 | + [('green', {'Version': '1.1~beta', 'Depends': 'libc6 (>= 0.9), libgreen1'})], |
266 | + 'green 1.1~beta FAIL green 1.1~beta\n', |
267 | + VALID_CANDIDATE, |
268 | + [r'\bgreen\b.*>1</a> to .*>1.1~beta<', |
269 | + '<li>autopkgtest for green 1.1~beta: %s' % ADT_EXCUSES_LABELS['ALWAYSFAIL']]) |
270 | + |
271 | + def test_request_for_installable_fail_regression(self): |
272 | '''Requests a test for an installable package, test fail''' |
273 | |
274 | self.do_test( |
275 | @@ -255,7 +295,8 @@ |
276 | 'green 1.1~beta FAIL green 1.1~beta\n', |
277 | NOT_CONSIDERED, |
278 | [r'\bgreen\b.*>1</a> to .*>1.1~beta<', |
279 | - '<li>autopkgtest for green 1.1~beta: FAIL']) |
280 | + '<li>autopkgtest for green 1.1~beta: %s' % ADT_EXCUSES_LABELS['REGRESSION']], |
281 | + history='green 1.0~beta PASS green 1.0~beta\n') |
282 | |
283 | def test_request_for_installable_pass(self): |
284 | '''Requests a test for an installable package, test pass''' |
285 | @@ -265,7 +306,7 @@ |
286 | 'green 1.1~beta PASS green 1.1~beta\n', |
287 | VALID_CANDIDATE, |
288 | [r'\bgreen\b.*>1</a> to .*>1.1~beta<', |
289 | - '<li>autopkgtest for green 1.1~beta: PASS']) |
290 | + '<li>autopkgtest for green 1.1~beta: %s' % ADT_EXCUSES_LABELS['PASS']]) |
291 | |
292 | def test_multi_rdepends_with_tests_running(self): |
293 | '''Multiple reverse dependencies with tests (still running)''' |
294 | @@ -276,10 +317,22 @@ |
295 | 'darkgreen 1 RUNNING green 2\n', |
296 | NOT_CONSIDERED, |
297 | [r'\bgreen\b.*>1</a> to .*>2<', |
298 | - '<li>autopkgtest for lightgreen 1: PASS', |
299 | - '<li>autopkgtest for darkgreen 1: RUNNING']) |
300 | - |
301 | - def test_multi_rdepends_with_tests_fail(self): |
302 | + '<li>autopkgtest for lightgreen 1: %s' % ADT_EXCUSES_LABELS['PASS'], |
303 | + '<li>autopkgtest for darkgreen 1: %s' % ADT_EXCUSES_LABELS['RUNNING']]) |
304 | + |
305 | + def test_multi_rdepends_with_tests_fail_always(self): |
306 | + '''Multiple reverse dependencies with tests (fail)''' |
307 | + |
308 | + self.do_test( |
309 | + [('libgreen1', {'Version': '2', 'Source': 'green', 'Depends': 'libc6'})], |
310 | + 'lightgreen 1 PASS green 2\n' |
311 | + 'darkgreen 1 FAIL green 2\n', |
312 | + VALID_CANDIDATE, |
313 | + [r'\bgreen\b.*>1</a> to .*>2<', |
314 | + '<li>autopkgtest for lightgreen 1: %s' % ADT_EXCUSES_LABELS['PASS'], |
315 | + '<li>autopkgtest for darkgreen 1: %s' % ADT_EXCUSES_LABELS['ALWAYSFAIL']]) |
316 | + |
317 | + def test_multi_rdepends_with_tests_fail_regression(self): |
318 | '''Multiple reverse dependencies with tests (fail)''' |
319 | |
320 | self.do_test( |
321 | @@ -288,8 +341,9 @@ |
322 | 'darkgreen 1 FAIL green 2\n', |
323 | NOT_CONSIDERED, |
324 | [r'\bgreen\b.*>1</a> to .*>2<', |
325 | - '<li>autopkgtest for lightgreen 1: PASS', |
326 | - '<li>autopkgtest for darkgreen 1: FAIL']) |
327 | + '<li>autopkgtest for lightgreen 1: %s' % ADT_EXCUSES_LABELS['PASS'], |
328 | + '<li>autopkgtest for darkgreen 1: %s' % ADT_EXCUSES_LABELS['REGRESSION']], |
329 | + history='darkgreen 1 PASS green 1\n') |
330 | |
331 | def test_multi_rdepends_with_tests_pass(self): |
332 | '''Multiple reverse dependencies with tests (pass)''' |
333 | @@ -300,8 +354,8 @@ |
334 | 'darkgreen 1 PASS green 2\n', |
335 | VALID_CANDIDATE, |
336 | [r'\bgreen\b.*>1</a> to .*>2<', |
337 | - '<li>autopkgtest for lightgreen 1: PASS', |
338 | - '<li>autopkgtest for darkgreen 1: PASS']) |
339 | + '<li>autopkgtest for lightgreen 1: %s' % ADT_EXCUSES_LABELS['PASS'], |
340 | + '<li>autopkgtest for darkgreen 1: %s' % ADT_EXCUSES_LABELS['PASS']]) |
341 | |
342 | def test_multi_rdepends_with_some_tests_running(self): |
343 | '''Multiple reverse dependencies with some tests (running)''' |
344 | @@ -315,10 +369,25 @@ |
345 | 'darkgreen 1 RUNNING green 2\n', |
346 | NOT_CONSIDERED, |
347 | [r'\bgreen\b.*>1</a> to .*>2<', |
348 | - '<li>autopkgtest for lightgreen 1: RUNNING', |
349 | - '<li>autopkgtest for darkgreen 1: RUNNING']) |
350 | - |
351 | - def test_multi_rdepends_with_some_tests_fail(self): |
352 | + '<li>autopkgtest for lightgreen 1: %s' % ADT_EXCUSES_LABELS['RUNNING'], |
353 | + '<li>autopkgtest for darkgreen 1: %s' % ADT_EXCUSES_LABELS['RUNNING']]) |
354 | + |
355 | + def test_multi_rdepends_with_some_tests_fail_always(self): |
356 | + '''Multiple reverse dependencies with some tests (fail)''' |
357 | + |
358 | + # add a third reverse dependency to libgreen1 which does not have a test |
359 | + self.data.add('mint', False, {'Depends': 'libgreen1'}) |
360 | + |
361 | + self.do_test( |
362 | + [('libgreen1', {'Version': '2', 'Source': 'green', 'Depends': 'libc6'})], |
363 | + 'lightgreen 1 PASS green 2\n' |
364 | + 'darkgreen 1 FAIL green 2\n', |
365 | + VALID_CANDIDATE, |
366 | + [r'\bgreen\b.*>1</a> to .*>2<', |
367 | + '<li>autopkgtest for lightgreen 1: %s' % ADT_EXCUSES_LABELS['PASS'], |
368 | + '<li>autopkgtest for darkgreen 1: %s' % ADT_EXCUSES_LABELS['ALWAYSFAIL']]) |
369 | + |
370 | + def test_multi_rdepends_with_some_tests_fail_regression(self): |
371 | '''Multiple reverse dependencies with some tests (fail)''' |
372 | |
373 | # add a third reverse dependency to libgreen1 which does not have a test |
374 | @@ -330,8 +399,9 @@ |
375 | 'darkgreen 1 FAIL green 2\n', |
376 | NOT_CONSIDERED, |
377 | [r'\bgreen\b.*>1</a> to .*>2<', |
378 | - '<li>autopkgtest for lightgreen 1: PASS', |
379 | - '<li>autopkgtest for darkgreen 1: FAIL']) |
380 | + '<li>autopkgtest for lightgreen 1: %s' % ADT_EXCUSES_LABELS['PASS'], |
381 | + '<li>autopkgtest for darkgreen 1: %s' % ADT_EXCUSES_LABELS['REGRESSION']], |
382 | + history='darkgreen 1 PASS green 1\n') |
383 | |
384 | def test_multi_rdepends_with_some_tests_pass(self): |
385 | '''Multiple reverse dependencies with some tests (pass)''' |
386 | @@ -345,8 +415,8 @@ |
387 | 'darkgreen 1 PASS green 2\n', |
388 | VALID_CANDIDATE, |
389 | [r'\bgreen\b.*>1</a> to .*>2<', |
390 | - '<li>autopkgtest for lightgreen 1: PASS', |
391 | - '<li>autopkgtest for darkgreen 1: PASS']) |
392 | + '<li>autopkgtest for lightgreen 1: %s' % ADT_EXCUSES_LABELS['PASS'], |
393 | + '<li>autopkgtest for darkgreen 1: %s' % ADT_EXCUSES_LABELS['PASS']]) |
394 | |
395 | def test_binary_from_new_source_package_running(self): |
396 | '''building an existing binary for a new source package (running)''' |
397 | @@ -357,10 +427,22 @@ |
398 | 'darkgreen 1 RUNNING newgreen 2\n', |
399 | NOT_CONSIDERED, |
400 | [r'\bnewgreen\b.*\(- to .*>2<', |
401 | - '<li>autopkgtest for lightgreen 1: PASS', |
402 | - '<li>autopkgtest for darkgreen 1: RUNNING']) |
403 | - |
404 | - def test_binary_from_new_source_package_fail(self): |
405 | + '<li>autopkgtest for lightgreen 1: %s' % ADT_EXCUSES_LABELS['PASS'], |
406 | + '<li>autopkgtest for darkgreen 1: %s' % ADT_EXCUSES_LABELS['RUNNING']]) |
407 | + |
408 | + def test_binary_from_new_source_package_fail_always(self): |
409 | + '''building an existing binary for a new source package (fail)''' |
410 | + |
411 | + self.do_test( |
412 | + [('libgreen1', {'Version': '2', 'Source': 'newgreen', 'Depends': 'libc6'})], |
413 | + 'lightgreen 1 PASS newgreen 2\n' |
414 | + 'darkgreen 1 FAIL newgreen 2\n', |
415 | + VALID_CANDIDATE, |
416 | + [r'\bnewgreen\b.*\(- to .*>2<', |
417 | + '<li>autopkgtest for lightgreen 1: %s' % ADT_EXCUSES_LABELS['PASS'], |
418 | + '<li>autopkgtest for darkgreen 1: %s' % ADT_EXCUSES_LABELS['ALWAYSFAIL']]) |
419 | + |
420 | + def test_binary_from_new_source_package_fail_regression(self): |
421 | '''building an existing binary for a new source package (fail)''' |
422 | |
423 | self.do_test( |
424 | @@ -369,8 +451,9 @@ |
425 | 'darkgreen 1 FAIL newgreen 2\n', |
426 | NOT_CONSIDERED, |
427 | [r'\bnewgreen\b.*\(- to .*>2<', |
428 | - '<li>autopkgtest for lightgreen 1: PASS', |
429 | - '<li>autopkgtest for darkgreen 1: FAIL']) |
430 | + '<li>autopkgtest for lightgreen 1: %s' % ADT_EXCUSES_LABELS['PASS'], |
431 | + '<li>autopkgtest for darkgreen 1: %s' % ADT_EXCUSES_LABELS['REGRESSION']], |
432 | + history='darkgreen 1 PASS green 1\n') |
433 | |
434 | def test_binary_from_new_source_package_pass(self): |
435 | '''building an existing binary for a new source package (pass)''' |
436 | @@ -381,8 +464,8 @@ |
437 | 'darkgreen 1 PASS newgreen 2\n', |
438 | VALID_CANDIDATE, |
439 | [r'\bnewgreen\b.*\(- to .*>2<', |
440 | - '<li>autopkgtest for lightgreen 1: PASS', |
441 | - '<li>autopkgtest for darkgreen 1: PASS']) |
442 | + '<li>autopkgtest for lightgreen 1: %s' % ADT_EXCUSES_LABELS['PASS'], |
443 | + '<li>autopkgtest for darkgreen 1: %s' % ADT_EXCUSES_LABELS['PASS']]) |
444 | |
445 | def test_binary_from_new_source_package_uninst(self): |
446 | '''building an existing binary for a new source package (uninstallable)''' |
447 | @@ -406,14 +489,76 @@ |
448 | NOT_CONSIDERED, |
449 | [r'\bgreen\b.*>1</a> to .*>1.1~beta<', |
450 | # it's not entirely clear what precisely it should say here |
451 | - '<li>autopkgtest for green 1.1~beta: RUNNING']) |
452 | + '<li>autopkgtest for green 1.1~beta: %s' % ADT_EXCUSES_LABELS['RUNNING']]) |
453 | + |
454 | + def test_request_for_installable_fail_regression_promoted(self): |
455 | + '''Requests a test for an installable package, test fail, is a regression. |
456 | + |
457 | + This test verifies a bug in britney where a package was promoted if latest test |
458 | + appeared before previous result in history, only the last result in |
459 | + alphabetic order was taken into account. For example: |
460 | + A 1 FAIL B 1 |
461 | + A 1 PASS A 1 |
462 | + In this case results for A 1 didn't appear in the list of results |
463 | + triggered by the upload of B 1 and B 1 was promoted |
464 | + ''' |
465 | + |
466 | + self.do_test( |
467 | + [('green', {'Version': '1.1~beta', 'Depends': 'libc6 (>= 0.9), libgreen1'})], |
468 | + 'lightgreen 1 FAIL green 1.1~beta\n', |
469 | + NOT_CONSIDERED, |
470 | + [r'\bgreen\b.*>1</a> to .*>1.1~beta<', |
471 | + '<li>autopkgtest for lightgreen 1: %s' % ADT_EXCUSES_LABELS['REGRESSION']], |
472 | + history="lightgreen 1 PASS lightgreen 1" |
473 | + ) |
474 | + |
475 | + def test_history_always_passed(self): |
476 | + '''All the results in history are PASS, and test passed |
477 | + |
478 | + ''' |
479 | + |
480 | + self.do_test( |
481 | + [('green', {'Version': '1.1~beta', 'Depends': 'libc6 (>= 0.9), libgreen1'})], |
482 | + 'lightgreen 1 PASS green 1.1~beta\n', |
483 | + VALID_CANDIDATE, |
484 | + [r'\bgreen\b.*>1</a> to .*>1.1~beta<', |
485 | + '<li>autopkgtest for lightgreen 1: %s' % ADT_EXCUSES_LABELS['PASS']], |
486 | + history="lightgreen 1 PASS lightgreen 1" |
487 | + ) |
488 | + |
489 | + def test_history_always_failed(self): |
490 | + '''All the results in history are FAIL, test fails. not a regression. |
491 | + |
492 | + ''' |
493 | + |
494 | + self.do_test( |
495 | + [('green', {'Version': '1.1~beta', 'Depends': 'libc6 (>= 0.9), libgreen1'})], |
496 | + 'lightgreen 1 FAIL green 1.1~beta\n', |
497 | + VALID_CANDIDATE, |
498 | + [r'\bgreen\b.*>1</a> to .*>1.1~beta<', |
499 | + '<li>autopkgtest for lightgreen 1: %s' % ADT_EXCUSES_LABELS['ALWAYSFAIL']], |
500 | + history="lightgreen 1 FAIL lightgreen 1" |
501 | + ) |
502 | + |
503 | + def test_history_regression(self): |
504 | + '''All the results in history are PASS, test fails. Blocked. |
505 | + |
506 | + ''' |
507 | + self.do_test( |
508 | + [('green', {'Version': '1.1~beta', 'Depends': 'libc6 (>= 0.9), libgreen1'})], |
509 | + 'lightgreen 1 FAIL green 1.1~beta\n', |
510 | + NOT_CONSIDERED, |
511 | + [r'\bgreen\b.*>1</a> to .*>1.1~beta<', |
512 | + '<li>autopkgtest for lightgreen 1: %s' % ADT_EXCUSES_LABELS['REGRESSION']], |
513 | + history="lightgreen 1 PASS lightgreen 1" |
514 | + ) |
515 | |
516 | def do_test(self, unstable_add, adt_request, considered, expect=None, |
517 | - no_expect=None): |
518 | + no_expect=None, history=""): |
519 | for (pkg, fields) in unstable_add: |
520 | self.data.add(pkg, True, fields) |
521 | |
522 | - self.make_adt_britney(adt_request) |
523 | + self.make_adt_britney(adt_request, history) |
524 | |
525 | (excuses, out) = self.run_britney() |
526 | #print('-------\nexcuses: %s\n-----' % excuses) |
527 | @@ -437,7 +582,8 @@ |
528 | self.data.add('yellow', True, {'Version': '1.1~beta', |
529 | 'Depends': 'libc6 (>= 0.9), nosuchpkg'}) |
530 | |
531 | - self.make_adt_britney('yellow 1.1~beta RUNNING yellow 1.1~beta\n') |
532 | + self.make_adt_britney('yellow 1.1~beta RUNNING yellow 1.1~beta\n', |
533 | + 'purple 2 FAIL pink 3.0.~britney\n') |
534 | |
535 | print('run:\n%s -c %s\n' % (self.britney, self.britney_conf)) |
536 | subprocess.call(['bash', '-i'], cwd=self.data.path) |
Many thanks for this, Jean-Baptiste! Some comments:
* My original tests (from https:/ /code.launchpad .net/~canonical -platform- qa/britney/ tests/+ merge/207982) are not merged into britney2-ubuntu yet. I don't know whether this was due to lack of time, or the release team would prefer the tests to have a different form. In the latter case, I suggest merging this without the changes to tests/autopkgte st.py, and we apply the latter on top of my MP above. We shouldn't block this urgent fix on desinging a new test suite, and the existing one at least shows that it's working now.
* Thanks for introducing the "always fails" state. This introduces the additional requirement that when opening a release we need to copy the last successful result (if any) in Jenkins to the new release, so that we don't consider packages which succeeded in trsuty and now fail in utopic as "always failed". That's a reasonable thing to do, I just want to point it out explicitly.
Code review for the new tests:
* Checking the precise HTML formatting and colors in the tests seems a bit excessive to me, but I don't mind it much. But if you want to keep it, please don't duplicate the definition of ADT_EXCUSES_LABELS in the test but import it from autopkgtest (note, this might require renaming tests/autopkgte st.py to tests/test_ autopkgtest. py to avoid a namespace clash).
* __merge_records() could do with a docstring, as it's not quite clear what this does and why it's needed in a test. That's the sort of logic I'd expect in the actual code, but in the test suite the fake results should be pre-formatted correctly? Also, is it actually justified to assume any particular ordering in history.txt, or should autopkgtest.py not assume any and just order it by itself?
def test_request_ for_installable _fail_always( self):
'''Requests a test for an installable package, test fail'''
I think it would make sense to split this in two: One should be the code as it is right now (i. e. with empty history), and be called "test_request_ for_installable _first_ fail()" ; and another one with a history of just one (or perhaps two) FAILs, which is called "test_request_ for_installable _fail_always" (with adjusted docstring). I think they both should count as ALWAYSFAIL, but I think it's still important to see that both cases behave as expected.
+ def test_request_ for_installable _fail_regressio n_promoted( self):
+ '''Requests a test for an installable package, test fail, is a
+ regression.
Please no multi-line short docstrings, this causes truncated descriptions when running with -v and is also against PEP-8/looks ugly.
Some really small nitpicks:
+ ''' % {'py': sys.executable, 'path': self.data.path, 'rq':
Please move the 'rq': to the next line so that key and value are together, for better readability.
+ '<li>autopkgtest for green 1.1~beta: {}'.format( ADT_EXCUSES_ LABELS[ 'RUNNING' ])])
We use %s macros (or similar) and the % operator instead of {} and .format() in other places, so maybe this could be made consistent?
+ In this case results for A 1didn't appear in the list of results
missing space after '1'. Also, excess ...