Merge lp:~canonical-ci-engineering/britney/boottesting-support into lp:~ubuntu-release/britney/britney2-ubuntu

Proposed by Ursula Junque
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
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://code.launchpad.net/~cprov/britney/boottesting-support/+merge/247321, ownership changed so anyone part of the CI team could make requested changes, if any).

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.

To post a comment you must log in.
441. By Francis Ginther

Correct copy-n-paste of 'e.' for 'excuse.'.

Revision history for this message
Colin Watson (cjwatson) :
review: Needs Fixing
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.

Revision history for this message
Francis Ginther (fginther) wrote :

I have a couple of comments for clarification around the 'check'/'submit' interface. The bigger question is about using source packages instead of binaries.

review: Needs Information
448. By Celso Providelo

Request boottests for sources, not binaries. Jenkins glue will drive adt properly.

Revision history for this message
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/submit/collect).

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).

Revision history for this message
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.

Revision history for this message
Francis Ginther (fginther) wrote :

Looks pretty good. My prior comments have been addressed and I don't see any issues with the proposed implementation.

review: Approve
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.

Revision history for this message
Colin Watson (cjwatson) wrote :

This looks pretty good to me now; just a few nits.

review: Approve
Revision history for this message
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/ubuntu-archive/proposed-migration/code/b2/britney.py", line 3259, in <module>
      Britney().main()
    File "/home/ubuntu-archive/proposed-migration/code/b2/britney.py", line 3211, in main
      self.write_excuses()
    File "/home/ubuntu-archive/proposed-migration/code/b2/britney.py", line 1982, in write_excuses
      upgrade_me.remove(excuse.name)
  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

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== added file 'boottest.py'
--- boottest.py 1970-01-01 00:00:00 +0000
+++ boottest.py 2015-02-18 13:50:50 +0000
@@ -0,0 +1,282 @@
1# -*- coding: utf-8 -*-
2
3# Copyright (C) 2015 Canonical Ltd.
4
5# This program is free software; you can redistribute it and/or modify
6# it under the terms of the GNU General Public License as published by
7# the Free Software Foundation; either version 2 of the License, or
8# (at your option) any later version.
9
10# This program is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13# GNU General Public License for more details.
14from __future__ import print_function
15
16from collections import defaultdict
17from contextlib import closing
18import os
19import subprocess
20import tempfile
21from textwrap import dedent
22import time
23import urllib
24
25import apt_pkg
26
27from consts import BINARIES
28
29
30FETCH_RETRIES = 3
31
32
33class TouchManifest(object):
34 """Parses a corresponding touch image manifest.
35
36 Based on http://cdimage.u.c/ubuntu-touch/daily-preinstalled/pending/vivid-preinstalled-touch-armhf.manifest
37
38 Assumes the deployment is arranged in a way the manifest is available
39 and fresh on:
40
41 '{britney_cwd}/boottest/images/{distribution}/{series}/manifest'
42
43 Only binary name matters, version is ignored, so callsites can:
44
45 >>> manifest = TouchManifest('ubuntu-touch', 'vivid')
46 >>> 'webbrowser-app' in manifest
47 True
48 >>> 'firefox' in manifest
49 False
50
51 """
52
53 def __init__(self, project, series, verbose=False, fetch=True):
54 self.verbose = verbose
55 self.path = "boottest/images/{}/{}/manifest".format(
56 project, series)
57 success = False
58 if fetch:
59 retries = FETCH_RETRIES
60 success = self.__fetch_manifest(project, series)
61
62 while retries > 0 and not success:
63 success = self.__fetch_manifest(project, series)
64 retries -= 1
65 if not success:
66 print("E: [%s] - Unable to fetch manifest: %s %s" % (
67 time.asctime(), project, series))
68
69 self._manifest = self._load()
70
71 def __fetch_manifest(self, project, series):
72 url = "http://cdimage.ubuntu.com/{}/daily-preinstalled/" \
73 "pending/{}-preinstalled-touch-armhf.manifest".format(
74 project, series
75 )
76 success = False
77 if self.verbose:
78 print(
79 "I: [%s] - Fetching manifest from %s" % (
80 time.asctime(), url))
81 print("I: [%s] - saving it to %s" % (time.asctime(), self.path))
82 try:
83 response = urllib.urlopen(url)
84 except IOError as e:
85 print("W: [%s] - error connecting to %s: %s" % (
86 time.asctime(), self.path, e))
87 return success # failure
88
89 # Only [re]create the manifest file if one was successfully downloaded
90 # this allows for an existing image to be used if the download fails.
91 if response.code == 200:
92 path_dir = os.path.dirname(self.path)
93 if not os.path.exists(path_dir):
94 os.makedirs(path_dir)
95 with open(self.path, 'w') as fp:
96 fp.write(response.read())
97 success = True
98
99 return success
100
101 def _load(self):
102 pkg_list = []
103
104 if not os.path.exists(self.path):
105 return pkg_list
106
107 with open(self.path) as fd:
108 for line in fd.readlines():
109 # skip headers and metadata
110 if 'DOCTYPE' in line:
111 continue
112 name, version = line.split()
113 name = name.split(':')[0]
114 if name == 'click':
115 continue
116 pkg_list.append(name)
117
118 return sorted(pkg_list)
119
120 def __contains__(self, key):
121 return key in self._manifest
122
123
124class BootTest(object):
125 """Boottest criteria for Britney.
126
127 This class provides an API for handling the boottest-jenkins
128 integration layer (mostly derived from auto-package-testing/adt):
129 """
130 VALID_STATUSES = ('PASS',)
131
132 EXCUSE_LABELS = {
133 "PASS": '<span style="background:#87d96c">Pass</span>',
134 "FAIL": '<span style="background:#ff6666">Regression</span>',
135 "RUNNING": '<span style="background:#99ddff">Test in progress</span>',
136 }
137
138 script_path = os.path.expanduser(
139 "~/auto-package-testing/jenkins/boottest-britney")
140
141 def __init__(self, britney, distribution, series, debug=False):
142 self.britney = britney
143 self.distribution = distribution
144 self.series = series
145 self.debug = debug
146 self.rc_path = None
147 self._read()
148 manifest_fetch = getattr(
149 self.britney.options, "boottest_fetch", "no") == "yes"
150 self.phone_manifest = TouchManifest(
151 'ubuntu-touch', self.series, fetch=manifest_fetch,
152 verbose=self.britney.options.verbose)
153
154 @property
155 def _request_path(self):
156 return "boottest/work/adt.request.%s" % self.series
157
158 @property
159 def _result_path(self):
160 return "boottest/work/adt.result.%s" % self.series
161
162 def _ensure_rc_file(self):
163 if self.rc_path:
164 return
165 self.rc_path = os.path.abspath("boottest/rc.%s" % self.series)
166 with open(self.rc_path, "w") as rc_file:
167 home = os.path.expanduser("~")
168 print(dedent("""\
169 release: %s
170 aptroot: ~/.chdist/%s-proposed-armhf/
171 apturi: file:%s/mirror/%s
172 components: main restricted universe multiverse
173 rsync_host: rsync://tachash.ubuntu-ci/boottest/
174 datadir: ~/proposed-migration/boottest/data""" %
175 (self.series, self.series, home, self.distribution)),
176 file=rc_file)
177
178 def _run(self, *args):
179 self._ensure_rc_file()
180 if not os.path.exists(self.script_path):
181 print("E: [%s] - Boottest/Jenking glue script missing: %s" % (
182 time.asctime(), self.script_path))
183 return '-'
184 command = [
185 self.script_path,
186 "-c", self.rc_path,
187 "-r", self.series,
188 "-PU",
189 ]
190 if self.debug:
191 command.append("-d")
192 command.extend(args)
193 return subprocess.check_output(command).strip()
194
195 def _read(self):
196 """Loads a list of results (sources tests and their status).
197
198 Provides internal data for `get_status()`.
199 """
200 self.pkglist = defaultdict(dict)
201 if not os.path.exists(self._result_path):
202 return
203 with open(self._result_path) as f:
204 for line in f:
205 line = line.strip()
206 if line.startswith("Suite:") or line.startswith("Date:"):
207 continue
208 linebits = line.split()
209 if len(linebits) < 2:
210 print("W: Invalid line format: '%s', skipped" % line)
211 continue
212 (src, ver, status) = linebits[:3]
213 if not (src in self.pkglist and ver in self.pkglist[src]):
214 self.pkglist[src][ver] = status
215
216 def get_status(self, name, version):
217 """Return test status for the given source name and version."""
218 try:
219 return self.pkglist[name][version]
220 except KeyError:
221 # This error handling accounts for outdated apt caches, when
222 # `boottest-britney` erroneously reports results for the
223 # current source version, instead of the proposed.
224 # Returning None here will block source promotion with:
225 # 'UNKNOWN STATUS' excuse. If the jobs are retried and its
226 # results find an up-to-date cache, the problem is gone.
227 print("E: [%s] - Missing boottest results for %s_%s" % (
228 time.asctime(), name, version))
229 return None
230
231 def request(self, packages):
232 """Requests boottests for the given sources list ([(src, ver),])."""
233 request_path = self._request_path
234 if os.path.exists(request_path):
235 os.unlink(request_path)
236 with closing(tempfile.NamedTemporaryFile(mode="w")) as request_file:
237 for src, ver in packages:
238 if src in self.pkglist and ver in self.pkglist[src]:
239 continue
240 print("%s %s" % (src, ver), file=request_file)
241 # Update 'pkglist' so even if submit/collect is not called
242 # (dry-run), britney has some results.
243 self.pkglist[src][ver] = 'RUNNING'
244 request_file.flush()
245 self._run("request", "-O", request_path, request_file.name)
246
247 def submit(self):
248 """Submits the current boottests requests for processing."""
249 self._run("submit", self._request_path)
250
251 def collect(self):
252 """Collects boottests results and updates internal registry."""
253 self._run("collect", "-O", self._result_path)
254 self._read()
255 if not self.britney.options.verbose:
256 return
257 for src in sorted(self.pkglist):
258 for ver in sorted(self.pkglist[src], cmp=apt_pkg.version_compare):
259 status = self.pkglist[src][ver]
260 print("I: [%s] - Collected boottest status for %s_%s: "
261 "%s" % (time.asctime(), src, ver, status))
262
263 def needs_test(self, name, version):
264 """Whether or not the given source and version should be tested.
265
266 Sources are only considered for boottesting if they produce binaries
267 that are part of the phone image manifest. See `TouchManifest`.
268 """
269 # Discover all binaries for the 'excused' source.
270 unstable_sources = self.britney.sources['unstable']
271 # Dismiss if source is not yet recognized (??).
272 if name not in unstable_sources:
273 return False
274 # Binaries are a seq of "<binname>/<arch>" and, practically, boottest
275 # is only concerned about armhf binaries mentioned in the phone
276 # manifest. Anything else should be skipped.
277 phone_binaries = [
278 b for b in unstable_sources[name][BINARIES]
279 if b.split('/')[1] in self.britney.options.boottest_arches.split()
280 and b.split('/')[0] in self.phone_manifest
281 ]
282 return bool(phone_binaries)
0283
=== modified file 'britney.conf'
--- britney.conf 2014-12-18 15:42:20 +0000
+++ britney.conf 2015-02-18 13:50:50 +0000
@@ -65,3 +65,8 @@
65ADT_ENABLE = yes65ADT_ENABLE = yes
66ADT_DEBUG = no66ADT_DEBUG = no
67ADT_ARCHES = amd64 i38667ADT_ARCHES = amd64 i386
68
69BOOTTEST_ENABLE = yes
70BOOTTEST_DEBUG = yes
71BOOTTEST_ARCHES = armhf amd64
72BOOTTEST_FETCH = yes
6873
=== modified file 'britney.py'
--- britney.py 2015-01-09 11:57:33 +0000
+++ britney.py 2015-02-18 13:50:50 +0000
@@ -226,6 +226,8 @@
226 SOURCE, SOURCEVER, ARCHITECTURE, DEPENDS, CONFLICTS,226 SOURCE, SOURCEVER, ARCHITECTURE, DEPENDS, CONFLICTS,
227 PROVIDES, RDEPENDS, RCONFLICTS, MULTIARCH, ESSENTIAL)227 PROVIDES, RDEPENDS, RCONFLICTS, MULTIARCH, ESSENTIAL)
228from autopkgtest import AutoPackageTest, ADT_PASS, ADT_EXCUSES_LABELS228from autopkgtest import AutoPackageTest, ADT_PASS, ADT_EXCUSES_LABELS
229from boottest import BootTest
230
229231
230__author__ = 'Fabio Tranchitella and the Debian Release Team'232__author__ = 'Fabio Tranchitella and the Debian Release Team'
231__version__ = '2.0'233__version__ = '2.0'
@@ -1366,6 +1368,7 @@
1366 # the starting point is that we will update the candidate and run autopkgtests1368 # the starting point is that we will update the candidate and run autopkgtests
1367 update_candidate = True1369 update_candidate = True
1368 run_autopkgtest = True1370 run_autopkgtest = True
1371 run_boottest = True
1369 1372
1370 # if the version in unstable is older, then stop here with a warning in the excuse and return False1373 # if the version in unstable is older, then stop here with a warning in the excuse and return False
1371 if source_t and apt_pkg.version_compare(source_u[VERSION], source_t[VERSION]) < 0:1374 if source_t and apt_pkg.version_compare(source_u[VERSION], source_t[VERSION]) < 0:
@@ -1379,6 +1382,7 @@
1379 excuse.addhtml("%s source package doesn't exist" % (src))1382 excuse.addhtml("%s source package doesn't exist" % (src))
1380 update_candidate = False1383 update_candidate = False
1381 run_autopkgtest = False1384 run_autopkgtest = False
1385 run_boottest = False
13821386
1383 # retrieve the urgency for the upload, ignoring it if this is a NEW package (not present in testing)1387 # retrieve the urgency for the upload, ignoring it if this is a NEW package (not present in testing)
1384 urgency = self.urgencies.get(src, self.options.default_urgency)1388 urgency = self.urgencies.get(src, self.options.default_urgency)
@@ -1397,6 +1401,7 @@
1397 excuse.addreason("remove")1401 excuse.addreason("remove")
1398 update_candidate = False1402 update_candidate = False
1399 run_autopkgtest = False1403 run_autopkgtest = False
1404 run_boottest = False
14001405
1401 # check if there is a `block' or `block-udeb' hint for this package, or a `block-all source' hint1406 # check if there is a `block' or `block-udeb' hint for this package, or a `block-all source' hint
1402 blocked = {}1407 blocked = {}
@@ -1476,6 +1481,7 @@
1476 else:1481 else:
1477 update_candidate = False1482 update_candidate = False
1478 run_autopkgtest = False1483 run_autopkgtest = False
1484 run_boottest = False
1479 excuse.addreason("age")1485 excuse.addreason("age")
14801486
1481 if suite in ['pu', 'tpu']:1487 if suite in ['pu', 'tpu']:
@@ -1511,6 +1517,8 @@
1511 update_candidate = False1517 update_candidate = False
1512 if arch in self.options.adt_arches.split():1518 if arch in self.options.adt_arches.split():
1513 run_autopkgtest = False1519 run_autopkgtest = False
1520 if arch in self.options.boottest_arches.split():
1521 run_boottest = False
1514 excuse.addreason("arch")1522 excuse.addreason("arch")
1515 excuse.addreason("arch-%s" % arch)1523 excuse.addreason("arch-%s" % arch)
1516 excuse.addreason("build-arch")1524 excuse.addreason("build-arch")
@@ -1553,6 +1561,8 @@
1553 update_candidate = False1561 update_candidate = False
1554 if arch in self.options.adt_arches.split():1562 if arch in self.options.adt_arches.split():
1555 run_autopkgtest = False1563 run_autopkgtest = False
1564 if arch in self.options.boottest_arches.split():
1565 run_boottest = False
15561566
1557 # if there are out-of-date packages, warn about them in the excuse and set update_candidate1567 # if there are out-of-date packages, warn about them in the excuse and set update_candidate
1558 # to False to block the update; if the architecture where the package is out-of-date is1568 # to False to block the update; if the architecture where the package is out-of-date is
@@ -1579,6 +1589,8 @@
1579 update_candidate = False1589 update_candidate = False
1580 if arch in self.options.adt_arches.split():1590 if arch in self.options.adt_arches.split():
1581 run_autopkgtest = False1591 run_autopkgtest = False
1592 if arch in self.options.boottest_arches.split():
1593 run_boottest = False
1582 excuse.addreason("arch")1594 excuse.addreason("arch")
1583 excuse.addreason("arch-%s" % arch)1595 excuse.addreason("arch-%s" % arch)
1584 if uptodatebins:1596 if uptodatebins:
@@ -1596,11 +1608,13 @@
1596 excuse.addreason("no-binaries")1608 excuse.addreason("no-binaries")
1597 update_candidate = False1609 update_candidate = False
1598 run_autopkgtest = False1610 run_autopkgtest = False
1611 run_boottest = False
1599 elif not built_anywhere:1612 elif not built_anywhere:
1600 excuse.addhtml("%s has no up-to-date binaries on any arch" % src)1613 excuse.addhtml("%s has no up-to-date binaries on any arch" % src)
1601 excuse.addreason("no-binaries")1614 excuse.addreason("no-binaries")
1602 update_candidate = False1615 update_candidate = False
1603 run_autopkgtest = False1616 run_autopkgtest = False
1617 run_boottest = False
16041618
1605 # if the suite is unstable, then we have to check the release-critical bug lists before1619 # if the suite is unstable, then we have to check the release-critical bug lists before
1606 # updating testing; if the unstable package has RC bugs that do not apply to the testing1620 # updating testing; if the unstable package has RC bugs that do not apply to the testing
@@ -1633,6 +1647,7 @@
1633 ["<a href=\"http://bugs.debian.org/%s\">#%s</a>" % (urllib.quote(a), a) for a in new_bugs])))1647 ["<a href=\"http://bugs.debian.org/%s\">#%s</a>" % (urllib.quote(a), a) for a in new_bugs])))
1634 update_candidate = False1648 update_candidate = False
1635 run_autopkgtest = False1649 run_autopkgtest = False
1650 run_boottest = False
1636 excuse.addreason("buggy")1651 excuse.addreason("buggy")
16371652
1638 if len(old_bugs) > 0:1653 if len(old_bugs) > 0:
@@ -1651,6 +1666,7 @@
1651 excuse.force()1666 excuse.force()
1652 update_candidate = True1667 update_candidate = True
1653 run_autopkgtest = True1668 run_autopkgtest = True
1669 run_boottest = True
16541670
1655 # if the package can be updated, it is a valid candidate1671 # if the package can be updated, it is a valid candidate
1656 if update_candidate:1672 if update_candidate:
@@ -1660,6 +1676,7 @@
1660 # TODO1676 # TODO
1661 excuse.addhtml("Not considered")1677 excuse.addhtml("Not considered")
1662 excuse.run_autopkgtest = run_autopkgtest1678 excuse.run_autopkgtest = run_autopkgtest
1679 excuse.run_boottest = run_boottest
16631680
1664 self.excuses.append(excuse)1681 self.excuses.append(excuse)
1665 return update_candidate1682 return update_candidate
@@ -1883,6 +1900,88 @@
1883 e.addreason("autopkgtest")1900 e.addreason("autopkgtest")
1884 e.is_valid = False1901 e.is_valid = False
18851902
1903 if (getattr(self.options, "boottest_enable", "no") == "yes" and
1904 self.options.series):
1905 # trigger 'boottest'ing for valid candidates.
1906 boottest_debug = getattr(
1907 self.options, "boottest_debug", "no") == "yes"
1908 boottest = BootTest(
1909 self, self.options.distribution, self.options.series,
1910 debug=boottest_debug)
1911 boottest_excuses = []
1912 for excuse in self.excuses:
1913 # Skip already invalid excuses.
1914 if not excuse.run_boottest:
1915 continue
1916 # Also skip removals, binary-only candidates, proposed-updates
1917 # and unknown versions.
1918 if (excuse.name.startswith("-") or
1919 "/" in excuse.name or
1920 "_" in excuse.name or
1921 excuse.ver[1] == "-"):
1922 continue
1923 # Allows hints to skip boottest attempts
1924 hints = self.hints.search(
1925 'force-skiptest', package=excuse.name)
1926 forces = [x for x in hints
1927 if same_source(excuse.ver[1], x.version)]
1928 if forces:
1929 excuse.addhtml(
1930 "boottest skipped from hints by %s" % forces[0].user)
1931 continue
1932 # Only sources whitelisted in the boottest context should
1933 # be tested (currently only sources building phone binaries).
1934 if not boottest.needs_test(excuse.name, excuse.ver[1]):
1935 # Silently skipping.
1936 continue
1937 # Okay, aggregate required boottests requests.
1938 boottest_excuses.append(excuse)
1939 boottest.request([(e.name, e.ver[1]) for e in boottest_excuses])
1940 # Dry-run avoids data exchange with external systems.
1941 if not self.options.dry_run:
1942 boottest.submit()
1943 boottest.collect()
1944 # Boottest Jenkins views location.
1945 jenkins_public = "https://jenkins.qa.ubuntu.com/job"
1946 jenkins_private = (
1947 "http://d-jenkins.ubuntu-ci:8080/view/%s/view/BootTest/job" %
1948 self.options.series.title())
1949 # Update excuses from the boottest context.
1950 for excuse in boottest_excuses:
1951 status = boottest.get_status(excuse.name, excuse.ver[1])
1952 label = BootTest.EXCUSE_LABELS.get(status, 'UNKNOWN STATUS')
1953 public_url = "%s/%s-boottest-%s/lastBuild" % (
1954 jenkins_public, self.options.series,
1955 excuse.name.replace("+", "-"))
1956 private_url = "%s/%s-boottest-%s/lastBuild" % (
1957 jenkins_private, self.options.series,
1958 excuse.name.replace("+", "-"))
1959 excuse.addhtml(
1960 "Boottest result: %s (Jenkins: <a href=\"%s\">public</a>"
1961 ", <a href=\"%s\">private</a>)" % (
1962 label, public_url, private_url))
1963 # Allows hints to force boottest failures/attempts
1964 # to be ignored.
1965 hints = self.hints.search('force', package=excuse.name)
1966 hints.extend(
1967 self.hints.search('force-badtest', package=excuse.name))
1968 forces = [x for x in hints
1969 if same_source(excuse.ver[1], x.version)]
1970 if forces:
1971 excuse.addhtml(
1972 "Should wait for %s %s boottest, but forced by "
1973 "%s" % (excuse.name, excuse.ver[1],
1974 forces[0].user))
1975 continue
1976 # Block promotion if any boottests attempt has failed or
1977 # still in progress.
1978 if status not in BootTest.VALID_STATUSES:
1979 excuse.addhtml("Not considered")
1980 excuse.addreason("boottest")
1981 excuse.is_valid = False
1982 upgrade_me.remove(excuse.name)
1983 unconsidered.append(excuse.name)
1984
1886 # invalidate impossible excuses1985 # invalidate impossible excuses
1887 for e in self.excuses:1986 for e in self.excuses:
1888 # parts[0] == package name1987 # parts[0] == package name
18891988
=== modified file 'excuse.py'
--- excuse.py 2014-12-10 10:36:01 +0000
+++ excuse.py 2015-02-18 13:50:50 +0000
@@ -1,6 +1,6 @@
1# -*- coding: utf-8 -*-1# -*- coding: utf-8 -*-
22
3# Copyright (C) 2001-2004 Anthony Towns <ajt@debian.org>3# Copyright (C) 2006, 2011-2015 Anthony Towns <ajt@debian.org>
4# Andreas Barth <aba@debian.org>4# Andreas Barth <aba@debian.org>
5# Fabio Tranchitella <kobold@debian.org>5# Fabio Tranchitella <kobold@debian.org>
66
@@ -52,6 +52,7 @@
52 self._dontinvalidate = False52 self._dontinvalidate = False
53 self.forced = False53 self.forced = False
54 self.run_autopkgtest = False54 self.run_autopkgtest = False
55 self.run_boottest = False
55 self.distribution = "ubuntu"56 self.distribution = "ubuntu"
5657
57 self.invalid_deps = []58 self.invalid_deps = []
5859
=== modified file 'tests/__init__.py'
--- tests/__init__.py 2015-02-18 13:50:50 +0000
+++ tests/__init__.py 2015-02-18 13:50:50 +0000
@@ -135,6 +135,11 @@
135 def tearDown(self):135 def tearDown(self):
136 del self.data136 del self.data
137137
138 def restore_config(self, content):
139 """Helper for restoring configuration contents on cleanup."""
140 with open(self.britney_conf, 'w') as fp:
141 fp.write(content)
142
138 def run_britney(self, args=[]):143 def run_britney(self, args=[]):
139 '''Run britney.144 '''Run britney.
140145
141146
=== modified file 'tests/test_autopkgtest.py'
--- tests/test_autopkgtest.py 2015-02-18 13:50:50 +0000
+++ tests/test_autopkgtest.py 2015-02-18 13:50:50 +0000
@@ -31,6 +31,16 @@
31 def setUp(self):31 def setUp(self):
32 super(TestAutoPkgTest, self).setUp()32 super(TestAutoPkgTest, self).setUp()
3333
34 # Mofify configuration according to the test context.
35 with open(self.britney_conf, 'r') as fp:
36 original_config = fp.read()
37 # Disable boottests.
38 new_config = original_config.replace(
39 'BOOTTEST_ENABLE = yes', 'BOOTTEST_ENABLE = no')
40 with open(self.britney_conf, 'w') as fp:
41 fp.write(new_config)
42 self.addCleanup(self.restore_config, original_config)
43
34 # fake adt-britney script44 # fake adt-britney script
35 self.adt_britney = os.path.join(45 self.adt_britney = os.path.join(
36 self.data.home, 'auto-package-testing', 'jenkins', 'adt-britney')46 self.data.home, 'auto-package-testing', 'jenkins', 'adt-britney')
3747
=== added file 'tests/test_boottest.py'
--- tests/test_boottest.py 1970-01-01 00:00:00 +0000
+++ tests/test_boottest.py 2015-02-18 13:50:50 +0000
@@ -0,0 +1,411 @@
1#!/usr/bin/python
2# (C) 2014 Canonical Ltd.
3#
4# This program is free software; you can redistribute it and/or modify
5# it under the terms of the GNU General Public License as published by
6# the Free Software Foundation; either version 2 of the License, or
7# (at your option) any later version.
8
9import mock
10import os
11import shutil
12import sys
13import tempfile
14import unittest
15
16
17PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
18sys.path.insert(0, PROJECT_DIR)
19
20import boottest
21from tests import TestBase
22
23
24def create_manifest(manifest_dir, lines):
25 """Helper function for writing touch image manifests."""
26 os.makedirs(manifest_dir)
27 with open(os.path.join(manifest_dir, 'manifest'), 'w') as fd:
28 fd.write('\n'.join(lines))
29
30
31class FakeResponse(object):
32
33 def __init__(self, code=404, content=''):
34 self.code = code
35 self.content = content
36
37 def read(self):
38 return self.content
39
40
41class TestTouchManifest(unittest.TestCase):
42
43 def setUp(self):
44 super(TestTouchManifest, self).setUp()
45 self.path = tempfile.mkdtemp(prefix='boottest')
46 os.chdir(self.path)
47 self.imagesdir = os.path.join(self.path, 'boottest/images')
48 os.makedirs(self.imagesdir)
49 self.addCleanup(shutil.rmtree, self.path)
50 _p = mock.patch('urllib.urlopen')
51 self.mocked_urlopen = _p.start()
52 self.mocked_urlopen.side_effect = [FakeResponse(code=404),]
53 self.addCleanup(_p.stop)
54 self.fetch_retries_orig = boottest.FETCH_RETRIES
55 def restore_fetch_retries():
56 boottest.FETCH_RETRIES = self.fetch_retries_orig
57 boottest.FETCH_RETRIES = 0
58 self.addCleanup(restore_fetch_retries)
59
60 def test_missing(self):
61 # Missing manifest file silently results in empty contents.
62 manifest = boottest.TouchManifest('I-dont-exist', 'vivid')
63 self.assertEqual([], manifest._manifest)
64 self.assertNotIn('foo', manifest)
65
66 def test_fetch(self):
67 # Missing manifest file is fetched dynamically
68 self.mocked_urlopen.side_effect = [
69 FakeResponse(code=200, content='foo 1.0'),
70 ]
71 manifest = boottest.TouchManifest('ubuntu-touch', 'vivid')
72 self.assertNotEqual([], manifest._manifest)
73
74 def test_fetch_disabled(self):
75 # Manifest auto-fetching can be disabled.
76 manifest = boottest.TouchManifest('ubuntu-touch', 'vivid', fetch=False)
77 self.mocked_urlopen.assert_not_called()
78 self.assertEqual([], manifest._manifest)
79
80 def test_fetch_fails(self):
81 project = 'fake'
82 series = 'fake'
83 manifest_dir = os.path.join(self.imagesdir, project, series)
84 manifest_lines = [
85 'foo:armhf 1~beta1',
86 ]
87 create_manifest(manifest_dir, manifest_lines)
88 manifest = boottest.TouchManifest(project, series)
89 self.assertEqual(1, len(manifest._manifest))
90 self.assertIn('foo', manifest)
91
92 def test_fetch_exception(self):
93 self.mocked_urlopen.side_effect = [IOError("connection refused")]
94 manifest = boottest.TouchManifest('not-real', 'not-real')
95 self.assertEqual(0, len(manifest._manifest))
96
97 def test_simple(self):
98 # Existing manifest file allows callsites to properly check presence.
99 manifest_dir = os.path.join(self.imagesdir, 'ubuntu/vivid')
100 manifest_lines = [
101 'bar 1234',
102 'foo:armhf 1~beta1',
103 'boing1-1.2\t666',
104 'click:com.ubuntu.shorts 0.2.346'
105 ]
106 create_manifest(manifest_dir, manifest_lines)
107
108 manifest = boottest.TouchManifest('ubuntu', 'vivid')
109 # We can dig deeper on the manifest package names list ...
110 self.assertEqual(
111 ['bar', 'boing1-1.2', 'foo'], manifest._manifest)
112 # but the '<name> in manifest' API reads better.
113 self.assertIn('foo', manifest)
114 self.assertIn('boing1-1.2', manifest)
115 self.assertNotIn('baz', manifest)
116 # 'click' name is blacklisted due to the click package syntax.
117 self.assertNotIn('click', manifest)
118
119
120class TestBoottestEnd2End(TestBase):
121 """End2End tests (calling `britney`) for the BootTest criteria."""
122
123 def setUp(self):
124 super(TestBoottestEnd2End, self).setUp()
125
126 # Modify shared configuration file.
127 with open(self.britney_conf, 'r') as fp:
128 original_config = fp.read()
129 # Disable autopkgtests.
130 new_config = original_config.replace(
131 'ADT_ENABLE = yes', 'ADT_ENABLE = no')
132 # Disable TouchManifest auto-fetching.
133 new_config = new_config.replace(
134 'BOOTTEST_FETCH = yes', 'BOOTTEST_FETCH = no')
135 with open(self.britney_conf, 'w') as fp:
136 fp.write(new_config)
137 self.addCleanup(self.restore_config, original_config)
138
139 self.data.add('libc6', False, {'Architecture': 'armhf'}),
140
141 self.data.add(
142 'libgreen1',
143 False,
144 {'Source': 'green', 'Architecture': 'armhf',
145 'Depends': 'libc6 (>= 0.9)'})
146 self.data.add(
147 'green',
148 False,
149 {'Source': 'green', 'Architecture': 'armhf',
150 'Depends': 'libc6 (>= 0.9), libgreen1'})
151 self.create_manifest([
152 'green 1.0',
153 'pyqt5:armhf 1.0',
154 'signon 1.0'
155 ])
156
157 def create_manifest(self, lines):
158 """Create a manifest for this britney run context."""
159 path = os.path.join(
160 self.data.path,
161 'boottest/images/ubuntu-touch/{}'.format(self.data.series))
162 create_manifest(path, lines)
163
164 def make_boottest(self):
165 """Create a stub version of boottest-britney script."""
166 script_path = os.path.expanduser(
167 "~/auto-package-testing/jenkins/boottest-britney")
168 os.makedirs(os.path.dirname(script_path))
169 with open(script_path, 'w') as f:
170 f.write('''#!%(py)s
171import argparse
172import os
173import shutil
174import sys
175
176template = """
177green 1.1~beta RUNNING
178pyqt5-src 1.1~beta PASS
179pyqt5-src 1.1 FAIL
180signon 1.1 PASS
181"""
182
183def request():
184 work_path = os.path.dirname(args.output)
185 os.makedirs(work_path)
186 shutil.copy(args.input, os.path.join(work_path, 'test_input'))
187 with open(args.output, 'w') as f:
188 f.write(template)
189
190def submit():
191 pass
192
193def collect():
194 with open(args.output, 'w') as f:
195 f.write(template)
196
197p = argparse.ArgumentParser()
198p.add_argument('-r')
199p.add_argument('-c')
200p.add_argument('-d', default=False, action='store_true')
201p.add_argument('-P', default=False, action='store_true')
202p.add_argument('-U', default=False, action='store_true')
203
204sp = p.add_subparsers()
205
206psubmit = sp.add_parser('submit')
207psubmit.add_argument('input')
208psubmit.set_defaults(func=submit)
209
210prequest = sp.add_parser('request')
211prequest.add_argument('-O', dest='output')
212prequest.add_argument('input')
213prequest.set_defaults(func=request)
214
215pcollect = sp.add_parser('collect')
216pcollect.add_argument('-O', dest='output')
217pcollect.set_defaults(func=collect)
218
219args = p.parse_args()
220args.func()
221 ''' % {'py': sys.executable})
222 os.chmod(script_path, 0o755)
223
224 def do_test(self, context, expect=None, no_expect=None):
225 """Process the given package context and assert britney results."""
226 for (pkg, fields) in context:
227 self.data.add(pkg, True, fields)
228 self.make_boottest()
229 (excuses, out) = self.run_britney()
230 #print('-------\nexcuses: %s\n-----' % excuses)
231 if expect:
232 for re in expect:
233 self.assertRegexpMatches(excuses, re)
234 if no_expect:
235 for re in no_expect:
236 self.assertNotRegexpMatches(excuses, re)
237
238 def test_runs(self):
239 # `Britney` runs and considers binary packages for boottesting
240 # when it is enabled in the configuration, only binaries needed
241 # in the phone image are considered for boottesting.
242 # The boottest status is presented along with its corresponding
243 # jenkins job urls for the public and the private servers.
244 # 'in progress' tests blocks package promotion.
245 context = [
246 ('green', {'Source': 'green', 'Version': '1.1~beta',
247 'Architecture': 'armhf', 'Depends': 'libc6 (>= 0.9)'}),
248 ('libgreen1', {'Source': 'green', 'Version': '1.1~beta',
249 'Architecture': 'armhf',
250 'Depends': 'libc6 (>= 0.9)'}),
251 ]
252 public_jenkins_url = (
253 'https://jenkins.qa.ubuntu.com/job/series-boottest-green/'
254 'lastBuild')
255 private_jenkins_url = (
256 'http://d-jenkins.ubuntu-ci:8080/view/Series/view/BootTest/'
257 'job/series-boottest-green/lastBuild')
258 self.do_test(
259 context,
260 [r'\bgreen\b.*>1</a> to .*>1.1~beta<',
261 r'<li>Boottest result: {} \(Jenkins: '
262 r'<a href="{}">public</a>, <a href="{}">private</a>\)'.format(
263 boottest.BootTest.EXCUSE_LABELS['RUNNING'],
264 public_jenkins_url, private_jenkins_url),
265 '<li>Not considered'])
266
267 # The `boottest-britney` input (recorded for testing purposes),
268 # contains a line matching the requested boottest attempt.
269 # '<source> <version>\n'
270 test_input_path = os.path.join(
271 self.data.path, 'boottest/work/test_input')
272 self.assertEqual(
273 ['green 1.1~beta\n'], open(test_input_path).readlines())
274
275 def test_pass(self):
276 # `Britney` updates boottesting information in excuses when the
277 # package test pass and marks the package as a valid candidate for
278 # promotion.
279 context = []
280 context.append(
281 ('signon', {'Version': '1.1', 'Architecture': 'armhf'}))
282 self.do_test(
283 context,
284 [r'\bsignon\b.*\(- to .*>1.1<',
285 '<li>Boottest result: {}'.format(
286 boottest.BootTest.EXCUSE_LABELS['PASS']),
287 '<li>Valid candidate'])
288
289 def test_fail(self):
290 # `Britney` updates boottesting information in excuses when the
291 # package test fails and blocks the package promotion
292 # ('Not considered.')
293 context = []
294 context.append(
295 ('pyqt5', {'Source': 'pyqt5-src', 'Version': '1.1',
296 'Architecture': 'all'}))
297 self.do_test(
298 context,
299 [r'\bpyqt5-src\b.*\(- to .*>1.1<',
300 '<li>Boottest result: {}'.format(
301 boottest.BootTest.EXCUSE_LABELS['FAIL']),
302 '<li>Not considered'])
303
304 def test_unknown(self):
305 # `Britney` does not block on missing boottest results for a
306 # particular source/version, in this case pyqt5-src_1.2 (not
307 # listed in the testing result history). Instead it renders
308 # excuses with 'UNKNOWN STATUS' and links to the corresponding
309 # jenkins jobs for further investigation. Source promotion is
310 # blocked, though.
311 context = [
312 ('pyqt5', {'Source': 'pyqt5-src', 'Version': '1.2',
313 'Architecture': 'armhf'})]
314 self.do_test(
315 context,
316 [r'\bpyqt5-src\b.*\(- to .*>1.2<',
317 r'<li>Boottest result: UNKNOWN STATUS \(Jenkins: .*\)',
318 '<li>Not considered'])
319
320 def create_hint(self, username, content):
321 """Populates a hint file for the given 'username' with 'content'."""
322 hints_path = os.path.join(
323 self.data.path,
324 'data/{}-proposed/Hints/{}'.format(self.data.series, username))
325 with open(hints_path, 'w') as fd:
326 fd.write(content)
327
328 def test_skipped_by_hints(self):
329 # `Britney` allows boottests to be skipped by hinting the
330 # corresponding source with 'force-skiptest'. The boottest
331 # attempt will not be requested.
332 context = [
333 ('pyqt5', {'Source': 'pyqt5-src', 'Version': '1.1',
334 'Architecture': 'all'}),
335 ]
336 self.create_hint('cjwatson', 'force-skiptest pyqt5-src/1.1')
337 self.do_test(
338 context,
339 [r'\bpyqt5-src\b.*\(- to .*>1.1<',
340 '<li>boottest skipped from hints by cjwatson',
341 '<li>Valid candidate'])
342
343 def test_fail_but_forced_by_hints(self):
344 # `Britney` allows boottests results to be ignored by hinting the
345 # corresponding source with 'force' or 'force-badtest'. The boottest
346 # attempt will still be requested and its results would be considered
347 # for other non-forced sources.
348 context = [
349 ('pyqt5', {'Source': 'pyqt5-src', 'Version': '1.1',
350 'Architecture': 'all'}),
351 ]
352 self.create_hint('cjwatson', 'force pyqt5-src/1.1')
353 self.do_test(
354 context,
355 [r'\bpyqt5-src\b.*\(- to .*>1.1<',
356 '<li>Boottest result: {}'.format(
357 boottest.BootTest.EXCUSE_LABELS['FAIL']),
358 '<li>Should wait for pyqt5-src 1.1 boottest, '
359 'but forced by cjwatson',
360 '<li>Valid candidate'])
361
362 def test_fail_but_ignored_by_hints(self):
363 # See `test_fail_but_forced_by_hints`.
364 context = [
365 ('green', {'Source': 'green', 'Version': '1.1~beta',
366 'Architecture': 'armhf', 'Depends': 'libc6 (>= 0.9)'}),
367 ]
368 self.create_hint('cjwatson', 'force-badtest green/1.1~beta')
369 self.do_test(
370 context,
371 [r'\bgreen\b.*>1</a> to .*>1.1~beta<',
372 '<li>Boottest result: {}'.format(
373 boottest.BootTest.EXCUSE_LABELS['RUNNING']),
374 '<li>Should wait for green 1.1~beta boottest, but forced '
375 'by cjwatson',
376 '<li>Valid candidate'])
377
378 def test_skipped_not_on_phone(self):
379 # `Britney` updates boottesting information in excuses when the
380 # package was skipped and marks the package as a valid candidate for
381 # promotion, but no notice about 'boottest' is added to the excuse.
382 context = []
383 context.append(
384 ('apache2', {'Source': 'apache2-src', 'Architecture': 'all',
385 'Version': '2.4.8-1ubuntu1'}))
386 self.do_test(
387 context,
388 [r'\bapache2-src\b.*\(- to .*>2.4.8-1ubuntu1<',
389 '<li>Valid candidate'],
390 ['<li>Boottest result:'],
391 )
392
393 def test_skipped_architecture_not_allowed(self):
394 # `Britney` does not trigger boottests for source not yet built on
395 # the allowed architectures.
396 self.data.add(
397 'pyqt5', False, {'Source': 'pyqt5-src', 'Architecture': 'armhf'})
398 context = [
399 ('pyqt5', {'Source': 'pyqt5-src', 'Version': '1.1',
400 'Architecture': 'amd64'}),
401 ]
402 self.do_test(
403 context,
404 [r'\bpyqt5-src\b.*>1</a> to .*>1.1<',
405 r'<li>missing build on .*>armhf</a>: pyqt5 \(from .*>1</a>\)',
406 '<li>Not considered'])
407
408
409
410if __name__ == '__main__':
411 unittest.main()

Subscribers

People subscribed via source and target branches