# -*- coding: utf-8 -*- # Copyright (C) 2015 Canonical Ltd. # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. from __future__ import print_function import os import subprocess import time import urllib from consts import BINARIES class TouchManifest(object): """Parses a corresponding touch image manifest. Based on http://cdimage.u.c/ubuntu-touch/daily-preinstalled/pending/vivid-preinstalled-touch-armhf.manifest Assumes the deployment is arranged in a way the manifest is available and fresh on: '{britney_cwd}/boottest/images/{distribution}/{series}/manifest' Only binary name matters, version is ignored, so callsites can: >>> manifest = TouchManifest('ubuntu', 'vivid') >>> 'webbrowser-app' in manifest True >>> 'firefox' in manifest False """ def __fetch_manifest(self, distribution, series): url = "http://cdimage.ubuntu.com/{}/daily-preinstalled/" \ "pending/{}-preinstalled-touch-armhf.manifest".format( distribution, series ) print("I: [%s] - Fetching manifest from %s" % (time.asctime(), url)) response = urllib.urlopen(url) # Only [re]create the manifest file if one was successfully downloaded # this allows for an existing image to be used if the download fails. if response.code == 200: os.makedirs(os.path.dirname(self.path)) with open(self.path, 'w') as fp: fp.write(response.read()) def __init__(self, distribution, series, fetch=True): self.path = "boottest/images/{}/{}/manifest".format( distribution, series) if fetch: self.__fetch_manifest(distribution, series) self._manifest = self._load() def _load(self): pkg_list = [] if not os.path.exists(self.path): return pkg_list with open(self.path) as fd: for line in fd.readlines(): # skip headers and metadata if 'DOCTYPE' in line: continue name, version = line.split() name = name.split(':')[0] if name == 'click': continue pkg_list.append(name) return sorted(pkg_list) def __contains__(self, key): return key in self._manifest class BootTestJenkinsJob(object): """Boottest - Jenkins **glue**. Wraps 'boottest/jenkins/boottest-britney' script for: * 'check' existing boottest job status ('check ') * 'submit' new boottest jobs ('submit ') """ script_path = "boottest/jenkins/boottest-britney" def __init__(self, distribution, series): self.distribution = distribution self.series = series def _run(self, *args): if not os.path.exists(self.script_path): print("E: [%s] - Boottest/Jenking glue script missing: %s" % ( time.asctime(), self.script_path)) return '-' command = [ self.script_path, "-d", self.distribution, "-s", self.series, ] command.extend(args) return subprocess.check_output(command).strip() def get_status(self, name, version): """Return the current boottest jenkins job status. Request a boottest attempt if it's new. """ try: status = self._run('check', name, version) except subprocess.CalledProcessError as err: status = self._run('submit', name, version) return status class BootTest(object): """Boottest criteria for Britney. Process (update) excuses for the 'boottest' criteria. Request and monitor boottest attempts (see `BootTestJenkinsJob`) for binaries present in the phone image manifest (see `TouchManifest`). """ VALID_STATUSES = ('PASS', 'SKIPPED') EXCUSE_LABELS = { "PASS": 'Pass', "SKIPPED": 'Skipped', "FAIL": 'Regression', "RUNNING": 'Test in progress', } def __init__(self, britney, distribution, series, debug=False): self.britney = britney self.distribution = distribution self.series = series self.debug = debug manifest_fetch = getattr( self.britney.options, "boottest_fetch", "no") == "yes" self.phone_manifest = TouchManifest( self.distribution, self.series, fetch=manifest_fetch) self.dispatcher = BootTestJenkinsJob(self.distribution, self.series) def update(self, excuse): """Update given 'excuse' and yields testing status. Yields (status, binary_name) for each binary considered for the given excuse. See `BootTestJenkinsJob.get_status`. Binaries are considered for boottesting if they are part of the phone image manifest. See `TouchManifest`. """ # Discover all binaries for the 'excused' source. unstable_sources = self.britney.sources['unstable'] # Dismiss if source is not yet recognized (??). if excuse.name not in unstable_sources: raise StopIteration # Binaries are a seq of "/" and, practically, boottest # is only concerned about armhf+all binaries. # Anything else should be skipped. binary_names = [ b.split('/')[0] for b in unstable_sources[excuse.name][BINARIES] if b.split('/')[1] in self.britney.options.boottest_arches.split() ] # Process (request or update) boottest attempts for each binary. for name in binary_names: if name in self.phone_manifest: status = self.dispatcher.get_status(name, excuse.ver[1]) else: status = 'SKIPPED' yield name, status