Merge lp:~canonical-ci-engineering/britney/boottesting-support into lp:~ubuntu-release/britney/britney2-ubuntu
- boottesting-support
- Merge into britney2-ubuntu
Status: | Merged |
---|---|
Merged at revision: | 430 |
Proposed branch: | lp:~canonical-ci-engineering/britney/boottesting-support |
Merge into: | lp:~ubuntu-release/britney/britney2-ubuntu |
Prerequisite: | lp:~cprov/britney/test-refactoring |
Diff against target: |
971 lines (+814/-1) 7 files modified
boottest.py (+282/-0) britney.conf (+5/-0) britney.py (+99/-0) excuse.py (+2/-1) tests/__init__.py (+5/-0) tests/test_autopkgtest.py (+10/-0) tests/test_boottest.py (+411/-0) |
To merge this branch: | bzr merge lp:~canonical-ci-engineering/britney/boottesting-support |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Colin Watson (community) | Approve | ||
Francis Ginther (community) | Approve | ||
Ubuntu Release Team | Pending | ||
Review via email: mp+247642@code.launchpad.net |
Commit message
Add support for submitting and collecting results for a boottest to be performed on packages that contribute binaries to the Ubuntu Touch image.
Description of the change
(This MP supersedes https:/
Basic "boottest" testing criteria support for britney.
It extends britney to submit and consider "boottest" test results before promoting uploads producing binaries that are included in the phone image.
- 441. By Francis Ginther
-
Correct copy-n-paste of 'e.' for 'excuse.'.
Colin Watson (cjwatson) : | # |
- 442. By Celso Providelo
-
Merging manifest auto-fetching changes
- 443. By Celso Providelo
-
Implement 'run_boottest' flag, similarly to 'run_autopkgset'.
- 444. By Celso Providelo
-
Introduce BOOTTEST_FETCH configuration option for enabling/disabling TouchManifest auto-fetching feature, for faster and isolated tests.
- 445. By Celso Providelo
-
Checking BOOTTEST_ARCHES binaries before attempting to boottest.
- 446. By Celso Providelo
-
Basic debug info when fetching manifests.
- 447. By Celso Providelo
-
Wraps 'boottest-britney' script for checking status or submitting new jobs.
- 448. By Celso Providelo
-
Request boottests for sources, not binaries. Jenkins glue will drive adt properly.
Celso Providelo (cprov) wrote : | # |
Francis,
Thanks for the review.
Now we are dispatching boottest request per SP, instead of its BPs.
I will work on changes for BootTestJenkinsJob to mimic the API already established for autopkgtest (request/
- 449. By Celso Providelo
-
Merge the boottest-jenkins script wrapper into BootTest class for simplicity and extend the API to match what we already for auto-package-
testing (ADT).
Celso Providelo (cprov) wrote : | # |
Francis,
Now BootTest criteria uses the established `boottest-jenkins` API (request, submit, collect) very similarly to the existing adt-jenkins.
The only exception, a simplification in fact, is that boottests does not map source triggers as autopkgtest, since the trigger for boottests is always the source itself.
- 450. By Celso Providelo
-
Cleanup the boottest-britney testing script to reflect better its real behavior.
- 451. By Celso Providelo
-
Adjusting boottest-jenkins arguments to match reality.
- 452. By Joe Talbott
-
boottest - Add retry loop (3) for manifest fetching.
- 453. By Joe Talbott
-
boottest - Only make the manifest directory if it doesn't already exist.
Francis Ginther (fginther) wrote : | # |
Looks pretty good. My prior comments have been addressed and I don't see any issues with the proposed implementation.
- 454. By Vincent Ladeuil
-
According to cjwatson, cdimage deals with projects, not distribution, fix TouchManifest accordingly.
Fix boottest-britney location to match production.
Fix TestTouchManifest test failures, now that we retry on manifest download errors, the tests should inhibit the retries when testing the failures.5 out of the 8 TestBoottestEnd2End are still failing because the excuse says the tests are skipped instead of running/
failing/ being in progress, etc. - 455. By Celso Providelo
-
[test-fix] Adjusting test setup for the new manifest path (using project) and the auto-package-
testing project path. - 456. By Celso Providelo
-
[test-fix] Fixing test_autopkgtest.py configuration setup, so all tests pass now.
- 457. By Joe Talbott
-
boottest - Add exception handling for manifest fetching
- 458. By Celso Providelo
-
Boottest integration adjustments.
- 459. By Celso Providelo
-
Presenting links for the corresponding boottest jenkins job.
- 460. By Celso Providelo
-
Suppress boottest SKIPPED notice on excuses, it's unnecessary noise.
- 461. By Celso Providelo
-
Cope with missing/broken results due to outdated apt cache. Britney will not fail, but source promotion will be blocked. Also waiting for amd64 binaries (arch-indep deps) before running boottests.
- 462. By Vincent Ladeuil
-
Poperly initialize Excuse.
run_boottest.
Colin Watson (cjwatson) wrote : | # |
This looks pretty good to me now; just a few nits.
Steve Langasek (vorlon) wrote : | # |
Have merged this, but have updated to set BOOTTEST_ENABLE = no due to the following error:
Traceback (most recent call last):
File "/home/
Britney(
File "/home/
self.
File "/home/
upgrade_
ValueError: list.remove(x): x not in list
Independently of this bug, it looks like we need BOOTTEST_ENABLE = no because the only way we reached this code block was because p-m had decided there was a non-empty set of packages that needed to be blocked on boot test results, which we of course don't have yet.
Preview Diff
1 | === added file 'boottest.py' |
2 | --- boottest.py 1970-01-01 00:00:00 +0000 |
3 | +++ boottest.py 2015-02-18 13:50:50 +0000 |
4 | @@ -0,0 +1,282 @@ |
5 | +# -*- coding: utf-8 -*- |
6 | + |
7 | +# Copyright (C) 2015 Canonical Ltd. |
8 | + |
9 | +# This program is free software; you can redistribute it and/or modify |
10 | +# it under the terms of the GNU General Public License as published by |
11 | +# the Free Software Foundation; either version 2 of the License, or |
12 | +# (at your option) any later version. |
13 | + |
14 | +# This program is distributed in the hope that it will be useful, |
15 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
16 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
17 | +# GNU General Public License for more details. |
18 | +from __future__ import print_function |
19 | + |
20 | +from collections import defaultdict |
21 | +from contextlib import closing |
22 | +import os |
23 | +import subprocess |
24 | +import tempfile |
25 | +from textwrap import dedent |
26 | +import time |
27 | +import urllib |
28 | + |
29 | +import apt_pkg |
30 | + |
31 | +from consts import BINARIES |
32 | + |
33 | + |
34 | +FETCH_RETRIES = 3 |
35 | + |
36 | + |
37 | +class TouchManifest(object): |
38 | + """Parses a corresponding touch image manifest. |
39 | + |
40 | + Based on http://cdimage.u.c/ubuntu-touch/daily-preinstalled/pending/vivid-preinstalled-touch-armhf.manifest |
41 | + |
42 | + Assumes the deployment is arranged in a way the manifest is available |
43 | + and fresh on: |
44 | + |
45 | + '{britney_cwd}/boottest/images/{distribution}/{series}/manifest' |
46 | + |
47 | + Only binary name matters, version is ignored, so callsites can: |
48 | + |
49 | + >>> manifest = TouchManifest('ubuntu-touch', 'vivid') |
50 | + >>> 'webbrowser-app' in manifest |
51 | + True |
52 | + >>> 'firefox' in manifest |
53 | + False |
54 | + |
55 | + """ |
56 | + |
57 | + def __init__(self, project, series, verbose=False, fetch=True): |
58 | + self.verbose = verbose |
59 | + self.path = "boottest/images/{}/{}/manifest".format( |
60 | + project, series) |
61 | + success = False |
62 | + if fetch: |
63 | + retries = FETCH_RETRIES |
64 | + success = self.__fetch_manifest(project, series) |
65 | + |
66 | + while retries > 0 and not success: |
67 | + success = self.__fetch_manifest(project, series) |
68 | + retries -= 1 |
69 | + if not success: |
70 | + print("E: [%s] - Unable to fetch manifest: %s %s" % ( |
71 | + time.asctime(), project, series)) |
72 | + |
73 | + self._manifest = self._load() |
74 | + |
75 | + def __fetch_manifest(self, project, series): |
76 | + url = "http://cdimage.ubuntu.com/{}/daily-preinstalled/" \ |
77 | + "pending/{}-preinstalled-touch-armhf.manifest".format( |
78 | + project, series |
79 | + ) |
80 | + success = False |
81 | + if self.verbose: |
82 | + print( |
83 | + "I: [%s] - Fetching manifest from %s" % ( |
84 | + time.asctime(), url)) |
85 | + print("I: [%s] - saving it to %s" % (time.asctime(), self.path)) |
86 | + try: |
87 | + response = urllib.urlopen(url) |
88 | + except IOError as e: |
89 | + print("W: [%s] - error connecting to %s: %s" % ( |
90 | + time.asctime(), self.path, e)) |
91 | + return success # failure |
92 | + |
93 | + # Only [re]create the manifest file if one was successfully downloaded |
94 | + # this allows for an existing image to be used if the download fails. |
95 | + if response.code == 200: |
96 | + path_dir = os.path.dirname(self.path) |
97 | + if not os.path.exists(path_dir): |
98 | + os.makedirs(path_dir) |
99 | + with open(self.path, 'w') as fp: |
100 | + fp.write(response.read()) |
101 | + success = True |
102 | + |
103 | + return success |
104 | + |
105 | + def _load(self): |
106 | + pkg_list = [] |
107 | + |
108 | + if not os.path.exists(self.path): |
109 | + return pkg_list |
110 | + |
111 | + with open(self.path) as fd: |
112 | + for line in fd.readlines(): |
113 | + # skip headers and metadata |
114 | + if 'DOCTYPE' in line: |
115 | + continue |
116 | + name, version = line.split() |
117 | + name = name.split(':')[0] |
118 | + if name == 'click': |
119 | + continue |
120 | + pkg_list.append(name) |
121 | + |
122 | + return sorted(pkg_list) |
123 | + |
124 | + def __contains__(self, key): |
125 | + return key in self._manifest |
126 | + |
127 | + |
128 | +class BootTest(object): |
129 | + """Boottest criteria for Britney. |
130 | + |
131 | + This class provides an API for handling the boottest-jenkins |
132 | + integration layer (mostly derived from auto-package-testing/adt): |
133 | + """ |
134 | + VALID_STATUSES = ('PASS',) |
135 | + |
136 | + EXCUSE_LABELS = { |
137 | + "PASS": '<span style="background:#87d96c">Pass</span>', |
138 | + "FAIL": '<span style="background:#ff6666">Regression</span>', |
139 | + "RUNNING": '<span style="background:#99ddff">Test in progress</span>', |
140 | + } |
141 | + |
142 | + script_path = os.path.expanduser( |
143 | + "~/auto-package-testing/jenkins/boottest-britney") |
144 | + |
145 | + def __init__(self, britney, distribution, series, debug=False): |
146 | + self.britney = britney |
147 | + self.distribution = distribution |
148 | + self.series = series |
149 | + self.debug = debug |
150 | + self.rc_path = None |
151 | + self._read() |
152 | + manifest_fetch = getattr( |
153 | + self.britney.options, "boottest_fetch", "no") == "yes" |
154 | + self.phone_manifest = TouchManifest( |
155 | + 'ubuntu-touch', self.series, fetch=manifest_fetch, |
156 | + verbose=self.britney.options.verbose) |
157 | + |
158 | + @property |
159 | + def _request_path(self): |
160 | + return "boottest/work/adt.request.%s" % self.series |
161 | + |
162 | + @property |
163 | + def _result_path(self): |
164 | + return "boottest/work/adt.result.%s" % self.series |
165 | + |
166 | + def _ensure_rc_file(self): |
167 | + if self.rc_path: |
168 | + return |
169 | + self.rc_path = os.path.abspath("boottest/rc.%s" % self.series) |
170 | + with open(self.rc_path, "w") as rc_file: |
171 | + home = os.path.expanduser("~") |
172 | + print(dedent("""\ |
173 | + release: %s |
174 | + aptroot: ~/.chdist/%s-proposed-armhf/ |
175 | + apturi: file:%s/mirror/%s |
176 | + components: main restricted universe multiverse |
177 | + rsync_host: rsync://tachash.ubuntu-ci/boottest/ |
178 | + datadir: ~/proposed-migration/boottest/data""" % |
179 | + (self.series, self.series, home, self.distribution)), |
180 | + file=rc_file) |
181 | + |
182 | + def _run(self, *args): |
183 | + self._ensure_rc_file() |
184 | + if not os.path.exists(self.script_path): |
185 | + print("E: [%s] - Boottest/Jenking glue script missing: %s" % ( |
186 | + time.asctime(), self.script_path)) |
187 | + return '-' |
188 | + command = [ |
189 | + self.script_path, |
190 | + "-c", self.rc_path, |
191 | + "-r", self.series, |
192 | + "-PU", |
193 | + ] |
194 | + if self.debug: |
195 | + command.append("-d") |
196 | + command.extend(args) |
197 | + return subprocess.check_output(command).strip() |
198 | + |
199 | + def _read(self): |
200 | + """Loads a list of results (sources tests and their status). |
201 | + |
202 | + Provides internal data for `get_status()`. |
203 | + """ |
204 | + self.pkglist = defaultdict(dict) |
205 | + if not os.path.exists(self._result_path): |
206 | + return |
207 | + with open(self._result_path) as f: |
208 | + for line in f: |
209 | + line = line.strip() |
210 | + if line.startswith("Suite:") or line.startswith("Date:"): |
211 | + continue |
212 | + linebits = line.split() |
213 | + if len(linebits) < 2: |
214 | + print("W: Invalid line format: '%s', skipped" % line) |
215 | + continue |
216 | + (src, ver, status) = linebits[:3] |
217 | + if not (src in self.pkglist and ver in self.pkglist[src]): |
218 | + self.pkglist[src][ver] = status |
219 | + |
220 | + def get_status(self, name, version): |
221 | + """Return test status for the given source name and version.""" |
222 | + try: |
223 | + return self.pkglist[name][version] |
224 | + except KeyError: |
225 | + # This error handling accounts for outdated apt caches, when |
226 | + # `boottest-britney` erroneously reports results for the |
227 | + # current source version, instead of the proposed. |
228 | + # Returning None here will block source promotion with: |
229 | + # 'UNKNOWN STATUS' excuse. If the jobs are retried and its |
230 | + # results find an up-to-date cache, the problem is gone. |
231 | + print("E: [%s] - Missing boottest results for %s_%s" % ( |
232 | + time.asctime(), name, version)) |
233 | + return None |
234 | + |
235 | + def request(self, packages): |
236 | + """Requests boottests for the given sources list ([(src, ver),]).""" |
237 | + request_path = self._request_path |
238 | + if os.path.exists(request_path): |
239 | + os.unlink(request_path) |
240 | + with closing(tempfile.NamedTemporaryFile(mode="w")) as request_file: |
241 | + for src, ver in packages: |
242 | + if src in self.pkglist and ver in self.pkglist[src]: |
243 | + continue |
244 | + print("%s %s" % (src, ver), file=request_file) |
245 | + # Update 'pkglist' so even if submit/collect is not called |
246 | + # (dry-run), britney has some results. |
247 | + self.pkglist[src][ver] = 'RUNNING' |
248 | + request_file.flush() |
249 | + self._run("request", "-O", request_path, request_file.name) |
250 | + |
251 | + def submit(self): |
252 | + """Submits the current boottests requests for processing.""" |
253 | + self._run("submit", self._request_path) |
254 | + |
255 | + def collect(self): |
256 | + """Collects boottests results and updates internal registry.""" |
257 | + self._run("collect", "-O", self._result_path) |
258 | + self._read() |
259 | + if not self.britney.options.verbose: |
260 | + return |
261 | + for src in sorted(self.pkglist): |
262 | + for ver in sorted(self.pkglist[src], cmp=apt_pkg.version_compare): |
263 | + status = self.pkglist[src][ver] |
264 | + print("I: [%s] - Collected boottest status for %s_%s: " |
265 | + "%s" % (time.asctime(), src, ver, status)) |
266 | + |
267 | + def needs_test(self, name, version): |
268 | + """Whether or not the given source and version should be tested. |
269 | + |
270 | + Sources are only considered for boottesting if they produce binaries |
271 | + that are part of the phone image manifest. See `TouchManifest`. |
272 | + """ |
273 | + # Discover all binaries for the 'excused' source. |
274 | + unstable_sources = self.britney.sources['unstable'] |
275 | + # Dismiss if source is not yet recognized (??). |
276 | + if name not in unstable_sources: |
277 | + return False |
278 | + # Binaries are a seq of "<binname>/<arch>" and, practically, boottest |
279 | + # is only concerned about armhf binaries mentioned in the phone |
280 | + # manifest. Anything else should be skipped. |
281 | + phone_binaries = [ |
282 | + b for b in unstable_sources[name][BINARIES] |
283 | + if b.split('/')[1] in self.britney.options.boottest_arches.split() |
284 | + and b.split('/')[0] in self.phone_manifest |
285 | + ] |
286 | + return bool(phone_binaries) |
287 | |
288 | === modified file 'britney.conf' |
289 | --- britney.conf 2014-12-18 15:42:20 +0000 |
290 | +++ britney.conf 2015-02-18 13:50:50 +0000 |
291 | @@ -65,3 +65,8 @@ |
292 | ADT_ENABLE = yes |
293 | ADT_DEBUG = no |
294 | ADT_ARCHES = amd64 i386 |
295 | + |
296 | +BOOTTEST_ENABLE = yes |
297 | +BOOTTEST_DEBUG = yes |
298 | +BOOTTEST_ARCHES = armhf amd64 |
299 | +BOOTTEST_FETCH = yes |
300 | |
301 | === modified file 'britney.py' |
302 | --- britney.py 2015-01-09 11:57:33 +0000 |
303 | +++ britney.py 2015-02-18 13:50:50 +0000 |
304 | @@ -226,6 +226,8 @@ |
305 | SOURCE, SOURCEVER, ARCHITECTURE, DEPENDS, CONFLICTS, |
306 | PROVIDES, RDEPENDS, RCONFLICTS, MULTIARCH, ESSENTIAL) |
307 | from autopkgtest import AutoPackageTest, ADT_PASS, ADT_EXCUSES_LABELS |
308 | +from boottest import BootTest |
309 | + |
310 | |
311 | __author__ = 'Fabio Tranchitella and the Debian Release Team' |
312 | __version__ = '2.0' |
313 | @@ -1366,6 +1368,7 @@ |
314 | # the starting point is that we will update the candidate and run autopkgtests |
315 | update_candidate = True |
316 | run_autopkgtest = True |
317 | + run_boottest = True |
318 | |
319 | # if the version in unstable is older, then stop here with a warning in the excuse and return False |
320 | if source_t and apt_pkg.version_compare(source_u[VERSION], source_t[VERSION]) < 0: |
321 | @@ -1379,6 +1382,7 @@ |
322 | excuse.addhtml("%s source package doesn't exist" % (src)) |
323 | update_candidate = False |
324 | run_autopkgtest = False |
325 | + run_boottest = False |
326 | |
327 | # retrieve the urgency for the upload, ignoring it if this is a NEW package (not present in testing) |
328 | urgency = self.urgencies.get(src, self.options.default_urgency) |
329 | @@ -1397,6 +1401,7 @@ |
330 | excuse.addreason("remove") |
331 | update_candidate = False |
332 | run_autopkgtest = False |
333 | + run_boottest = False |
334 | |
335 | # check if there is a `block' or `block-udeb' hint for this package, or a `block-all source' hint |
336 | blocked = {} |
337 | @@ -1476,6 +1481,7 @@ |
338 | else: |
339 | update_candidate = False |
340 | run_autopkgtest = False |
341 | + run_boottest = False |
342 | excuse.addreason("age") |
343 | |
344 | if suite in ['pu', 'tpu']: |
345 | @@ -1511,6 +1517,8 @@ |
346 | update_candidate = False |
347 | if arch in self.options.adt_arches.split(): |
348 | run_autopkgtest = False |
349 | + if arch in self.options.boottest_arches.split(): |
350 | + run_boottest = False |
351 | excuse.addreason("arch") |
352 | excuse.addreason("arch-%s" % arch) |
353 | excuse.addreason("build-arch") |
354 | @@ -1553,6 +1561,8 @@ |
355 | update_candidate = False |
356 | if arch in self.options.adt_arches.split(): |
357 | run_autopkgtest = False |
358 | + if arch in self.options.boottest_arches.split(): |
359 | + run_boottest = False |
360 | |
361 | # if there are out-of-date packages, warn about them in the excuse and set update_candidate |
362 | # to False to block the update; if the architecture where the package is out-of-date is |
363 | @@ -1579,6 +1589,8 @@ |
364 | update_candidate = False |
365 | if arch in self.options.adt_arches.split(): |
366 | run_autopkgtest = False |
367 | + if arch in self.options.boottest_arches.split(): |
368 | + run_boottest = False |
369 | excuse.addreason("arch") |
370 | excuse.addreason("arch-%s" % arch) |
371 | if uptodatebins: |
372 | @@ -1596,11 +1608,13 @@ |
373 | excuse.addreason("no-binaries") |
374 | update_candidate = False |
375 | run_autopkgtest = False |
376 | + run_boottest = False |
377 | elif not built_anywhere: |
378 | excuse.addhtml("%s has no up-to-date binaries on any arch" % src) |
379 | excuse.addreason("no-binaries") |
380 | update_candidate = False |
381 | run_autopkgtest = False |
382 | + run_boottest = False |
383 | |
384 | # if the suite is unstable, then we have to check the release-critical bug lists before |
385 | # updating testing; if the unstable package has RC bugs that do not apply to the testing |
386 | @@ -1633,6 +1647,7 @@ |
387 | ["<a href=\"http://bugs.debian.org/%s\">#%s</a>" % (urllib.quote(a), a) for a in new_bugs]))) |
388 | update_candidate = False |
389 | run_autopkgtest = False |
390 | + run_boottest = False |
391 | excuse.addreason("buggy") |
392 | |
393 | if len(old_bugs) > 0: |
394 | @@ -1651,6 +1666,7 @@ |
395 | excuse.force() |
396 | update_candidate = True |
397 | run_autopkgtest = True |
398 | + run_boottest = True |
399 | |
400 | # if the package can be updated, it is a valid candidate |
401 | if update_candidate: |
402 | @@ -1660,6 +1676,7 @@ |
403 | # TODO |
404 | excuse.addhtml("Not considered") |
405 | excuse.run_autopkgtest = run_autopkgtest |
406 | + excuse.run_boottest = run_boottest |
407 | |
408 | self.excuses.append(excuse) |
409 | return update_candidate |
410 | @@ -1883,6 +1900,88 @@ |
411 | e.addreason("autopkgtest") |
412 | e.is_valid = False |
413 | |
414 | + if (getattr(self.options, "boottest_enable", "no") == "yes" and |
415 | + self.options.series): |
416 | + # trigger 'boottest'ing for valid candidates. |
417 | + boottest_debug = getattr( |
418 | + self.options, "boottest_debug", "no") == "yes" |
419 | + boottest = BootTest( |
420 | + self, self.options.distribution, self.options.series, |
421 | + debug=boottest_debug) |
422 | + boottest_excuses = [] |
423 | + for excuse in self.excuses: |
424 | + # Skip already invalid excuses. |
425 | + if not excuse.run_boottest: |
426 | + continue |
427 | + # Also skip removals, binary-only candidates, proposed-updates |
428 | + # and unknown versions. |
429 | + if (excuse.name.startswith("-") or |
430 | + "/" in excuse.name or |
431 | + "_" in excuse.name or |
432 | + excuse.ver[1] == "-"): |
433 | + continue |
434 | + # Allows hints to skip boottest attempts |
435 | + hints = self.hints.search( |
436 | + 'force-skiptest', package=excuse.name) |
437 | + forces = [x for x in hints |
438 | + if same_source(excuse.ver[1], x.version)] |
439 | + if forces: |
440 | + excuse.addhtml( |
441 | + "boottest skipped from hints by %s" % forces[0].user) |
442 | + continue |
443 | + # Only sources whitelisted in the boottest context should |
444 | + # be tested (currently only sources building phone binaries). |
445 | + if not boottest.needs_test(excuse.name, excuse.ver[1]): |
446 | + # Silently skipping. |
447 | + continue |
448 | + # Okay, aggregate required boottests requests. |
449 | + boottest_excuses.append(excuse) |
450 | + boottest.request([(e.name, e.ver[1]) for e in boottest_excuses]) |
451 | + # Dry-run avoids data exchange with external systems. |
452 | + if not self.options.dry_run: |
453 | + boottest.submit() |
454 | + boottest.collect() |
455 | + # Boottest Jenkins views location. |
456 | + jenkins_public = "https://jenkins.qa.ubuntu.com/job" |
457 | + jenkins_private = ( |
458 | + "http://d-jenkins.ubuntu-ci:8080/view/%s/view/BootTest/job" % |
459 | + self.options.series.title()) |
460 | + # Update excuses from the boottest context. |
461 | + for excuse in boottest_excuses: |
462 | + status = boottest.get_status(excuse.name, excuse.ver[1]) |
463 | + label = BootTest.EXCUSE_LABELS.get(status, 'UNKNOWN STATUS') |
464 | + public_url = "%s/%s-boottest-%s/lastBuild" % ( |
465 | + jenkins_public, self.options.series, |
466 | + excuse.name.replace("+", "-")) |
467 | + private_url = "%s/%s-boottest-%s/lastBuild" % ( |
468 | + jenkins_private, self.options.series, |
469 | + excuse.name.replace("+", "-")) |
470 | + excuse.addhtml( |
471 | + "Boottest result: %s (Jenkins: <a href=\"%s\">public</a>" |
472 | + ", <a href=\"%s\">private</a>)" % ( |
473 | + label, public_url, private_url)) |
474 | + # Allows hints to force boottest failures/attempts |
475 | + # to be ignored. |
476 | + hints = self.hints.search('force', package=excuse.name) |
477 | + hints.extend( |
478 | + self.hints.search('force-badtest', package=excuse.name)) |
479 | + forces = [x for x in hints |
480 | + if same_source(excuse.ver[1], x.version)] |
481 | + if forces: |
482 | + excuse.addhtml( |
483 | + "Should wait for %s %s boottest, but forced by " |
484 | + "%s" % (excuse.name, excuse.ver[1], |
485 | + forces[0].user)) |
486 | + continue |
487 | + # Block promotion if any boottests attempt has failed or |
488 | + # still in progress. |
489 | + if status not in BootTest.VALID_STATUSES: |
490 | + excuse.addhtml("Not considered") |
491 | + excuse.addreason("boottest") |
492 | + excuse.is_valid = False |
493 | + upgrade_me.remove(excuse.name) |
494 | + unconsidered.append(excuse.name) |
495 | + |
496 | # invalidate impossible excuses |
497 | for e in self.excuses: |
498 | # parts[0] == package name |
499 | |
500 | === modified file 'excuse.py' |
501 | --- excuse.py 2014-12-10 10:36:01 +0000 |
502 | +++ excuse.py 2015-02-18 13:50:50 +0000 |
503 | @@ -1,6 +1,6 @@ |
504 | # -*- coding: utf-8 -*- |
505 | |
506 | -# Copyright (C) 2001-2004 Anthony Towns <ajt@debian.org> |
507 | +# Copyright (C) 2006, 2011-2015 Anthony Towns <ajt@debian.org> |
508 | # Andreas Barth <aba@debian.org> |
509 | # Fabio Tranchitella <kobold@debian.org> |
510 | |
511 | @@ -52,6 +52,7 @@ |
512 | self._dontinvalidate = False |
513 | self.forced = False |
514 | self.run_autopkgtest = False |
515 | + self.run_boottest = False |
516 | self.distribution = "ubuntu" |
517 | |
518 | self.invalid_deps = [] |
519 | |
520 | === modified file 'tests/__init__.py' |
521 | --- tests/__init__.py 2015-02-18 13:50:50 +0000 |
522 | +++ tests/__init__.py 2015-02-18 13:50:50 +0000 |
523 | @@ -135,6 +135,11 @@ |
524 | def tearDown(self): |
525 | del self.data |
526 | |
527 | + def restore_config(self, content): |
528 | + """Helper for restoring configuration contents on cleanup.""" |
529 | + with open(self.britney_conf, 'w') as fp: |
530 | + fp.write(content) |
531 | + |
532 | def run_britney(self, args=[]): |
533 | '''Run britney. |
534 | |
535 | |
536 | === modified file 'tests/test_autopkgtest.py' |
537 | --- tests/test_autopkgtest.py 2015-02-18 13:50:50 +0000 |
538 | +++ tests/test_autopkgtest.py 2015-02-18 13:50:50 +0000 |
539 | @@ -31,6 +31,16 @@ |
540 | def setUp(self): |
541 | super(TestAutoPkgTest, self).setUp() |
542 | |
543 | + # Mofify configuration according to the test context. |
544 | + with open(self.britney_conf, 'r') as fp: |
545 | + original_config = fp.read() |
546 | + # Disable boottests. |
547 | + new_config = original_config.replace( |
548 | + 'BOOTTEST_ENABLE = yes', 'BOOTTEST_ENABLE = no') |
549 | + with open(self.britney_conf, 'w') as fp: |
550 | + fp.write(new_config) |
551 | + self.addCleanup(self.restore_config, original_config) |
552 | + |
553 | # fake adt-britney script |
554 | self.adt_britney = os.path.join( |
555 | self.data.home, 'auto-package-testing', 'jenkins', 'adt-britney') |
556 | |
557 | === added file 'tests/test_boottest.py' |
558 | --- tests/test_boottest.py 1970-01-01 00:00:00 +0000 |
559 | +++ tests/test_boottest.py 2015-02-18 13:50:50 +0000 |
560 | @@ -0,0 +1,411 @@ |
561 | +#!/usr/bin/python |
562 | +# (C) 2014 Canonical Ltd. |
563 | +# |
564 | +# This program is free software; you can redistribute it and/or modify |
565 | +# it under the terms of the GNU General Public License as published by |
566 | +# the Free Software Foundation; either version 2 of the License, or |
567 | +# (at your option) any later version. |
568 | + |
569 | +import mock |
570 | +import os |
571 | +import shutil |
572 | +import sys |
573 | +import tempfile |
574 | +import unittest |
575 | + |
576 | + |
577 | +PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) |
578 | +sys.path.insert(0, PROJECT_DIR) |
579 | + |
580 | +import boottest |
581 | +from tests import TestBase |
582 | + |
583 | + |
584 | +def create_manifest(manifest_dir, lines): |
585 | + """Helper function for writing touch image manifests.""" |
586 | + os.makedirs(manifest_dir) |
587 | + with open(os.path.join(manifest_dir, 'manifest'), 'w') as fd: |
588 | + fd.write('\n'.join(lines)) |
589 | + |
590 | + |
591 | +class FakeResponse(object): |
592 | + |
593 | + def __init__(self, code=404, content=''): |
594 | + self.code = code |
595 | + self.content = content |
596 | + |
597 | + def read(self): |
598 | + return self.content |
599 | + |
600 | + |
601 | +class TestTouchManifest(unittest.TestCase): |
602 | + |
603 | + def setUp(self): |
604 | + super(TestTouchManifest, self).setUp() |
605 | + self.path = tempfile.mkdtemp(prefix='boottest') |
606 | + os.chdir(self.path) |
607 | + self.imagesdir = os.path.join(self.path, 'boottest/images') |
608 | + os.makedirs(self.imagesdir) |
609 | + self.addCleanup(shutil.rmtree, self.path) |
610 | + _p = mock.patch('urllib.urlopen') |
611 | + self.mocked_urlopen = _p.start() |
612 | + self.mocked_urlopen.side_effect = [FakeResponse(code=404),] |
613 | + self.addCleanup(_p.stop) |
614 | + self.fetch_retries_orig = boottest.FETCH_RETRIES |
615 | + def restore_fetch_retries(): |
616 | + boottest.FETCH_RETRIES = self.fetch_retries_orig |
617 | + boottest.FETCH_RETRIES = 0 |
618 | + self.addCleanup(restore_fetch_retries) |
619 | + |
620 | + def test_missing(self): |
621 | + # Missing manifest file silently results in empty contents. |
622 | + manifest = boottest.TouchManifest('I-dont-exist', 'vivid') |
623 | + self.assertEqual([], manifest._manifest) |
624 | + self.assertNotIn('foo', manifest) |
625 | + |
626 | + def test_fetch(self): |
627 | + # Missing manifest file is fetched dynamically |
628 | + self.mocked_urlopen.side_effect = [ |
629 | + FakeResponse(code=200, content='foo 1.0'), |
630 | + ] |
631 | + manifest = boottest.TouchManifest('ubuntu-touch', 'vivid') |
632 | + self.assertNotEqual([], manifest._manifest) |
633 | + |
634 | + def test_fetch_disabled(self): |
635 | + # Manifest auto-fetching can be disabled. |
636 | + manifest = boottest.TouchManifest('ubuntu-touch', 'vivid', fetch=False) |
637 | + self.mocked_urlopen.assert_not_called() |
638 | + self.assertEqual([], manifest._manifest) |
639 | + |
640 | + def test_fetch_fails(self): |
641 | + project = 'fake' |
642 | + series = 'fake' |
643 | + manifest_dir = os.path.join(self.imagesdir, project, series) |
644 | + manifest_lines = [ |
645 | + 'foo:armhf 1~beta1', |
646 | + ] |
647 | + create_manifest(manifest_dir, manifest_lines) |
648 | + manifest = boottest.TouchManifest(project, series) |
649 | + self.assertEqual(1, len(manifest._manifest)) |
650 | + self.assertIn('foo', manifest) |
651 | + |
652 | + def test_fetch_exception(self): |
653 | + self.mocked_urlopen.side_effect = [IOError("connection refused")] |
654 | + manifest = boottest.TouchManifest('not-real', 'not-real') |
655 | + self.assertEqual(0, len(manifest._manifest)) |
656 | + |
657 | + def test_simple(self): |
658 | + # Existing manifest file allows callsites to properly check presence. |
659 | + manifest_dir = os.path.join(self.imagesdir, 'ubuntu/vivid') |
660 | + manifest_lines = [ |
661 | + 'bar 1234', |
662 | + 'foo:armhf 1~beta1', |
663 | + 'boing1-1.2\t666', |
664 | + 'click:com.ubuntu.shorts 0.2.346' |
665 | + ] |
666 | + create_manifest(manifest_dir, manifest_lines) |
667 | + |
668 | + manifest = boottest.TouchManifest('ubuntu', 'vivid') |
669 | + # We can dig deeper on the manifest package names list ... |
670 | + self.assertEqual( |
671 | + ['bar', 'boing1-1.2', 'foo'], manifest._manifest) |
672 | + # but the '<name> in manifest' API reads better. |
673 | + self.assertIn('foo', manifest) |
674 | + self.assertIn('boing1-1.2', manifest) |
675 | + self.assertNotIn('baz', manifest) |
676 | + # 'click' name is blacklisted due to the click package syntax. |
677 | + self.assertNotIn('click', manifest) |
678 | + |
679 | + |
680 | +class TestBoottestEnd2End(TestBase): |
681 | + """End2End tests (calling `britney`) for the BootTest criteria.""" |
682 | + |
683 | + def setUp(self): |
684 | + super(TestBoottestEnd2End, self).setUp() |
685 | + |
686 | + # Modify shared configuration file. |
687 | + with open(self.britney_conf, 'r') as fp: |
688 | + original_config = fp.read() |
689 | + # Disable autopkgtests. |
690 | + new_config = original_config.replace( |
691 | + 'ADT_ENABLE = yes', 'ADT_ENABLE = no') |
692 | + # Disable TouchManifest auto-fetching. |
693 | + new_config = new_config.replace( |
694 | + 'BOOTTEST_FETCH = yes', 'BOOTTEST_FETCH = no') |
695 | + with open(self.britney_conf, 'w') as fp: |
696 | + fp.write(new_config) |
697 | + self.addCleanup(self.restore_config, original_config) |
698 | + |
699 | + self.data.add('libc6', False, {'Architecture': 'armhf'}), |
700 | + |
701 | + self.data.add( |
702 | + 'libgreen1', |
703 | + False, |
704 | + {'Source': 'green', 'Architecture': 'armhf', |
705 | + 'Depends': 'libc6 (>= 0.9)'}) |
706 | + self.data.add( |
707 | + 'green', |
708 | + False, |
709 | + {'Source': 'green', 'Architecture': 'armhf', |
710 | + 'Depends': 'libc6 (>= 0.9), libgreen1'}) |
711 | + self.create_manifest([ |
712 | + 'green 1.0', |
713 | + 'pyqt5:armhf 1.0', |
714 | + 'signon 1.0' |
715 | + ]) |
716 | + |
717 | + def create_manifest(self, lines): |
718 | + """Create a manifest for this britney run context.""" |
719 | + path = os.path.join( |
720 | + self.data.path, |
721 | + 'boottest/images/ubuntu-touch/{}'.format(self.data.series)) |
722 | + create_manifest(path, lines) |
723 | + |
724 | + def make_boottest(self): |
725 | + """Create a stub version of boottest-britney script.""" |
726 | + script_path = os.path.expanduser( |
727 | + "~/auto-package-testing/jenkins/boottest-britney") |
728 | + os.makedirs(os.path.dirname(script_path)) |
729 | + with open(script_path, 'w') as f: |
730 | + f.write('''#!%(py)s |
731 | +import argparse |
732 | +import os |
733 | +import shutil |
734 | +import sys |
735 | + |
736 | +template = """ |
737 | +green 1.1~beta RUNNING |
738 | +pyqt5-src 1.1~beta PASS |
739 | +pyqt5-src 1.1 FAIL |
740 | +signon 1.1 PASS |
741 | +""" |
742 | + |
743 | +def request(): |
744 | + work_path = os.path.dirname(args.output) |
745 | + os.makedirs(work_path) |
746 | + shutil.copy(args.input, os.path.join(work_path, 'test_input')) |
747 | + with open(args.output, 'w') as f: |
748 | + f.write(template) |
749 | + |
750 | +def submit(): |
751 | + pass |
752 | + |
753 | +def collect(): |
754 | + with open(args.output, 'w') as f: |
755 | + f.write(template) |
756 | + |
757 | +p = argparse.ArgumentParser() |
758 | +p.add_argument('-r') |
759 | +p.add_argument('-c') |
760 | +p.add_argument('-d', default=False, action='store_true') |
761 | +p.add_argument('-P', default=False, action='store_true') |
762 | +p.add_argument('-U', default=False, action='store_true') |
763 | + |
764 | +sp = p.add_subparsers() |
765 | + |
766 | +psubmit = sp.add_parser('submit') |
767 | +psubmit.add_argument('input') |
768 | +psubmit.set_defaults(func=submit) |
769 | + |
770 | +prequest = sp.add_parser('request') |
771 | +prequest.add_argument('-O', dest='output') |
772 | +prequest.add_argument('input') |
773 | +prequest.set_defaults(func=request) |
774 | + |
775 | +pcollect = sp.add_parser('collect') |
776 | +pcollect.add_argument('-O', dest='output') |
777 | +pcollect.set_defaults(func=collect) |
778 | + |
779 | +args = p.parse_args() |
780 | +args.func() |
781 | + ''' % {'py': sys.executable}) |
782 | + os.chmod(script_path, 0o755) |
783 | + |
784 | + def do_test(self, context, expect=None, no_expect=None): |
785 | + """Process the given package context and assert britney results.""" |
786 | + for (pkg, fields) in context: |
787 | + self.data.add(pkg, True, fields) |
788 | + self.make_boottest() |
789 | + (excuses, out) = self.run_britney() |
790 | + #print('-------\nexcuses: %s\n-----' % excuses) |
791 | + if expect: |
792 | + for re in expect: |
793 | + self.assertRegexpMatches(excuses, re) |
794 | + if no_expect: |
795 | + for re in no_expect: |
796 | + self.assertNotRegexpMatches(excuses, re) |
797 | + |
798 | + def test_runs(self): |
799 | + # `Britney` runs and considers binary packages for boottesting |
800 | + # when it is enabled in the configuration, only binaries needed |
801 | + # in the phone image are considered for boottesting. |
802 | + # The boottest status is presented along with its corresponding |
803 | + # jenkins job urls for the public and the private servers. |
804 | + # 'in progress' tests blocks package promotion. |
805 | + context = [ |
806 | + ('green', {'Source': 'green', 'Version': '1.1~beta', |
807 | + 'Architecture': 'armhf', 'Depends': 'libc6 (>= 0.9)'}), |
808 | + ('libgreen1', {'Source': 'green', 'Version': '1.1~beta', |
809 | + 'Architecture': 'armhf', |
810 | + 'Depends': 'libc6 (>= 0.9)'}), |
811 | + ] |
812 | + public_jenkins_url = ( |
813 | + 'https://jenkins.qa.ubuntu.com/job/series-boottest-green/' |
814 | + 'lastBuild') |
815 | + private_jenkins_url = ( |
816 | + 'http://d-jenkins.ubuntu-ci:8080/view/Series/view/BootTest/' |
817 | + 'job/series-boottest-green/lastBuild') |
818 | + self.do_test( |
819 | + context, |
820 | + [r'\bgreen\b.*>1</a> to .*>1.1~beta<', |
821 | + r'<li>Boottest result: {} \(Jenkins: ' |
822 | + r'<a href="{}">public</a>, <a href="{}">private</a>\)'.format( |
823 | + boottest.BootTest.EXCUSE_LABELS['RUNNING'], |
824 | + public_jenkins_url, private_jenkins_url), |
825 | + '<li>Not considered']) |
826 | + |
827 | + # The `boottest-britney` input (recorded for testing purposes), |
828 | + # contains a line matching the requested boottest attempt. |
829 | + # '<source> <version>\n' |
830 | + test_input_path = os.path.join( |
831 | + self.data.path, 'boottest/work/test_input') |
832 | + self.assertEqual( |
833 | + ['green 1.1~beta\n'], open(test_input_path).readlines()) |
834 | + |
835 | + def test_pass(self): |
836 | + # `Britney` updates boottesting information in excuses when the |
837 | + # package test pass and marks the package as a valid candidate for |
838 | + # promotion. |
839 | + context = [] |
840 | + context.append( |
841 | + ('signon', {'Version': '1.1', 'Architecture': 'armhf'})) |
842 | + self.do_test( |
843 | + context, |
844 | + [r'\bsignon\b.*\(- to .*>1.1<', |
845 | + '<li>Boottest result: {}'.format( |
846 | + boottest.BootTest.EXCUSE_LABELS['PASS']), |
847 | + '<li>Valid candidate']) |
848 | + |
849 | + def test_fail(self): |
850 | + # `Britney` updates boottesting information in excuses when the |
851 | + # package test fails and blocks the package promotion |
852 | + # ('Not considered.') |
853 | + context = [] |
854 | + context.append( |
855 | + ('pyqt5', {'Source': 'pyqt5-src', 'Version': '1.1', |
856 | + 'Architecture': 'all'})) |
857 | + self.do_test( |
858 | + context, |
859 | + [r'\bpyqt5-src\b.*\(- to .*>1.1<', |
860 | + '<li>Boottest result: {}'.format( |
861 | + boottest.BootTest.EXCUSE_LABELS['FAIL']), |
862 | + '<li>Not considered']) |
863 | + |
864 | + def test_unknown(self): |
865 | + # `Britney` does not block on missing boottest results for a |
866 | + # particular source/version, in this case pyqt5-src_1.2 (not |
867 | + # listed in the testing result history). Instead it renders |
868 | + # excuses with 'UNKNOWN STATUS' and links to the corresponding |
869 | + # jenkins jobs for further investigation. Source promotion is |
870 | + # blocked, though. |
871 | + context = [ |
872 | + ('pyqt5', {'Source': 'pyqt5-src', 'Version': '1.2', |
873 | + 'Architecture': 'armhf'})] |
874 | + self.do_test( |
875 | + context, |
876 | + [r'\bpyqt5-src\b.*\(- to .*>1.2<', |
877 | + r'<li>Boottest result: UNKNOWN STATUS \(Jenkins: .*\)', |
878 | + '<li>Not considered']) |
879 | + |
880 | + def create_hint(self, username, content): |
881 | + """Populates a hint file for the given 'username' with 'content'.""" |
882 | + hints_path = os.path.join( |
883 | + self.data.path, |
884 | + 'data/{}-proposed/Hints/{}'.format(self.data.series, username)) |
885 | + with open(hints_path, 'w') as fd: |
886 | + fd.write(content) |
887 | + |
888 | + def test_skipped_by_hints(self): |
889 | + # `Britney` allows boottests to be skipped by hinting the |
890 | + # corresponding source with 'force-skiptest'. The boottest |
891 | + # attempt will not be requested. |
892 | + context = [ |
893 | + ('pyqt5', {'Source': 'pyqt5-src', 'Version': '1.1', |
894 | + 'Architecture': 'all'}), |
895 | + ] |
896 | + self.create_hint('cjwatson', 'force-skiptest pyqt5-src/1.1') |
897 | + self.do_test( |
898 | + context, |
899 | + [r'\bpyqt5-src\b.*\(- to .*>1.1<', |
900 | + '<li>boottest skipped from hints by cjwatson', |
901 | + '<li>Valid candidate']) |
902 | + |
903 | + def test_fail_but_forced_by_hints(self): |
904 | + # `Britney` allows boottests results to be ignored by hinting the |
905 | + # corresponding source with 'force' or 'force-badtest'. The boottest |
906 | + # attempt will still be requested and its results would be considered |
907 | + # for other non-forced sources. |
908 | + context = [ |
909 | + ('pyqt5', {'Source': 'pyqt5-src', 'Version': '1.1', |
910 | + 'Architecture': 'all'}), |
911 | + ] |
912 | + self.create_hint('cjwatson', 'force pyqt5-src/1.1') |
913 | + self.do_test( |
914 | + context, |
915 | + [r'\bpyqt5-src\b.*\(- to .*>1.1<', |
916 | + '<li>Boottest result: {}'.format( |
917 | + boottest.BootTest.EXCUSE_LABELS['FAIL']), |
918 | + '<li>Should wait for pyqt5-src 1.1 boottest, ' |
919 | + 'but forced by cjwatson', |
920 | + '<li>Valid candidate']) |
921 | + |
922 | + def test_fail_but_ignored_by_hints(self): |
923 | + # See `test_fail_but_forced_by_hints`. |
924 | + context = [ |
925 | + ('green', {'Source': 'green', 'Version': '1.1~beta', |
926 | + 'Architecture': 'armhf', 'Depends': 'libc6 (>= 0.9)'}), |
927 | + ] |
928 | + self.create_hint('cjwatson', 'force-badtest green/1.1~beta') |
929 | + self.do_test( |
930 | + context, |
931 | + [r'\bgreen\b.*>1</a> to .*>1.1~beta<', |
932 | + '<li>Boottest result: {}'.format( |
933 | + boottest.BootTest.EXCUSE_LABELS['RUNNING']), |
934 | + '<li>Should wait for green 1.1~beta boottest, but forced ' |
935 | + 'by cjwatson', |
936 | + '<li>Valid candidate']) |
937 | + |
938 | + def test_skipped_not_on_phone(self): |
939 | + # `Britney` updates boottesting information in excuses when the |
940 | + # package was skipped and marks the package as a valid candidate for |
941 | + # promotion, but no notice about 'boottest' is added to the excuse. |
942 | + context = [] |
943 | + context.append( |
944 | + ('apache2', {'Source': 'apache2-src', 'Architecture': 'all', |
945 | + 'Version': '2.4.8-1ubuntu1'})) |
946 | + self.do_test( |
947 | + context, |
948 | + [r'\bapache2-src\b.*\(- to .*>2.4.8-1ubuntu1<', |
949 | + '<li>Valid candidate'], |
950 | + ['<li>Boottest result:'], |
951 | + ) |
952 | + |
953 | + def test_skipped_architecture_not_allowed(self): |
954 | + # `Britney` does not trigger boottests for source not yet built on |
955 | + # the allowed architectures. |
956 | + self.data.add( |
957 | + 'pyqt5', False, {'Source': 'pyqt5-src', 'Architecture': 'armhf'}) |
958 | + context = [ |
959 | + ('pyqt5', {'Source': 'pyqt5-src', 'Version': '1.1', |
960 | + 'Architecture': 'amd64'}), |
961 | + ] |
962 | + self.do_test( |
963 | + context, |
964 | + [r'\bpyqt5-src\b.*>1</a> to .*>1.1<', |
965 | + r'<li>missing build on .*>armhf</a>: pyqt5 \(from .*>1</a>\)', |
966 | + '<li>Not considered']) |
967 | + |
968 | + |
969 | + |
970 | +if __name__ == '__main__': |
971 | + unittest.main() |
I have a couple of comments for clarification around the 'check'/'submit' interface. The bigger question is about using source packages instead of binaries.