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

Subscribers

People subscribed via source and target branches