@ -13,11 +13,17 @@
# GNU General Public License for more details.
# GNU General Public License for more details.
from __future__ import print_function
from __future__ import print_function
from collections import defaultdict
from contextlib import closing
import os
import os
import subprocess
import subprocess
import tempfile
from textwrap import dedent
import time
import time
import urllib
import urllib
import apt_pkg
from consts import BINARIES
from consts import BINARIES
@ -41,12 +47,23 @@ class TouchManifest(object):
"""
"""
def __init__ ( self , distribution , series , verbose = False , fetch = True ) :
self . verbose = verbose
self . path = " boottest/images/ {} / {} /manifest " . format (
distribution , series )
if fetch :
self . __fetch_manifest ( distribution , series )
self . _manifest = self . _load ( )
def __fetch_manifest ( self , distribution , series ) :
def __fetch_manifest ( self , distribution , series ) :
url = " http://cdimage.ubuntu.com/ {} /daily-preinstalled/ " \
url = " http://cdimage.ubuntu.com/ {} /daily-preinstalled/ " \
" pending/ {} -preinstalled-touch-armhf.manifest " . format (
" pending/ {} -preinstalled-touch-armhf.manifest " . format (
distribution , series
distribution , series
)
)
print ( " I: [ %s ] - Fetching manifest from %s " % ( time . asctime ( ) , url ) )
if self . verbose :
print (
" I: [ %s ] - Fetching manifest from %s " % (
time . asctime ( ) , url ) )
response = urllib . urlopen ( url )
response = urllib . urlopen ( url )
# Only [re]create the manifest file if one was successfully downloaded
# Only [re]create the manifest file if one was successfully downloaded
# this allows for an existing image to be used if the download fails.
# this allows for an existing image to be used if the download fails.
@ -55,15 +72,6 @@ class TouchManifest(object):
with open ( self . path , ' w ' ) as fp :
with open ( self . path , ' w ' ) as fp :
fp . write ( response . read ( ) )
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 ) :
def _load ( self ) :
pkg_list = [ ]
pkg_list = [ ]
@ -87,104 +95,141 @@ class TouchManifest(object):
return key in self . _manifest
return key in self . _manifest
class BootTestJenkinsJob ( object ) :
class BootTest ( object ) :
""" Boottest - Jenkins **glue**.
""" Boottest criteria for Britney.
Wraps ' boottest/jenkins/boottest-britney ' script for :
* ' check ' existing boottest job status ( ' check <source> <version> ' )
* ' submit ' new boottest jobs ( ' submit <source> <version> ' )
This class provides an API for handling the boottest - jenkins
integration layer ( mostly derived from auto - package - testing / adt ) :
"""
"""
VALID_STATUSES = ( ' PASS ' , ' SKIPPED ' )
EXCUSE_LABELS = {
" PASS " : ' <span style= " background:#87d96c " >Pass</span> ' ,
" SKIPPED " : ' <span style= " background:#e5c545 " >Skipped</span> ' ,
" FAIL " : ' <span style= " background:#ff6666 " >Regression</span> ' ,
" RUNNING " : ' <span style= " background:#99ddff " >Test in progress</span> ' ,
}
script_path = " boottest/jenkins/boottest-britney "
script_path = " boottest/jenkins/boottest-britney "
def __init__ ( self , distribution , series ) :
def __init__ ( self , britney , distribution , series , debug = False ) :
self . britney = britney
self . distribution = distribution
self . distribution = distribution
self . series = series
self . series = series
self . debug = debug
self . rc_path = None
self . _read ( )
manifest_fetch = getattr (
self . britney . options , " boottest_fetch " , " no " ) == " yes "
self . phone_manifest = TouchManifest (
self . distribution , self . series , fetch = manifest_fetch ,
verbose = self . britney . options . verbose )
@property
def _request_path ( self ) :
return " boottest/work/adt.request. %s " % self . series
@property
def _result_path ( self ) :
return " boottest/work/adt.result. %s " % self . series
def _ensure_rc_file ( self ) :
if self . rc_path :
return
self . rc_path = os . path . abspath ( " boottest/rc. %s " % self . series )
with open ( self . rc_path , " w " ) as rc_file :
home = os . path . expanduser ( " ~ " )
print ( dedent ( """ \
release : % s
aptroot : ~ / . chdist / % s - proposed - armhf /
apturi : file : % s / mirror / % s
components : main restricted universe multiverse
rsync_host : rsync : / / tachash . ubuntu - ci / adt /
datadir : ~ / proposed - migration / boottest / data """ %
( self . series , self . series , home , self . distribution ) ) ,
file = rc_file )
def _run ( self , * args ) :
def _run ( self , * args ) :
self . _ensure_rc_file ( )
if not os . path . exists ( self . script_path ) :
if not os . path . exists ( self . script_path ) :
print ( " E: [ %s ] - Boottest/Jenking glue script missing: %s " % (
print ( " E: [ %s ] - Boottest/Jenking glue script missing: %s " % (
time . asctime ( ) , self . script_path ) )
time . asctime ( ) , self . script_path ) )
return ' - '
return ' - '
command = [
command = [
self . script_path ,
self . script_path ,
" -c " , self . rc_path ,
" -d " , self . distribution , " -s " , self . series ,
" -d " , self . distribution , " -s " , self . series ,
]
]
command . extend ( args )
command . extend ( args )
return subprocess . check_output ( command ) . strip ( )
return subprocess . check_output ( command ) . strip ( )
def get_status ( self , name , version ) :
def _read( self ) :
""" Return the current boottest jenkins job status.
""" Loads a list of results (sources tests and their status) .
Request a boottest attempt if it ' s new.
Provides internal data for ` get_status ( ) ` .
"""
"""
try :
self . pkglist = defaultdict ( dict )
status = self . _run ( ' check ' , name , version )
if not os . path . exists ( self . _result_path ) :
except subprocess . CalledProcessError as err :
return
status = self . _run ( ' submit ' , name , version )
with open ( self . _result_path ) as f :
return status
for line in f :
line = line . strip ( )
if line . startswith ( " Suite: " ) or line . startswith ( " Date: " ) :
class BootTest ( object ) :
continue
""" Boottest criteria for Britney.
linebits = line . split ( )
if len ( linebits ) < 2 :
Process ( update ) excuses for the ' boottest ' criteria . Request and monitor
print ( " W: Invalid line format: ' %s ' , skipped " % line )
boottest attempts ( see ` BootTestJenkinsJob ` ) for binaries present in the
continu e
phone image manifest ( see ` TouchManifest ` ) .
( src , ver , status ) = linebits [ : 3 ]
"""
if not ( src in self . pkglist and ver in self . pkglist [ src ] ) :
VALID_STATUSES = ( ' PASS ' , ' SKIPPED ' )
self . pkglist [ src ] [ ver ] = status
EXCUSE_LABELS = {
def get_status ( self , name , version ) :
" PASS " : ' <span style= " background:#87d96c " >Pass</span> ' ,
""" Return test status for the given source name and version. """
" SKIPPED " : ' <span style= " background:#e5c545 " >Skipped</span> ' ,
return self . pkglist [ name ] [ version ]
" FAIL " : ' <span style= " background:#ff6666 " >Regression</span> ' ,
" RUNNING " : ' <span style= " background:#99ddff " >Test in progress</span> ' ,
def request ( self , packages ) :
}
""" Requests boottests for the given sources list ([(src, ver),]). """
request_path = self . _request_path
if os . path . exists ( request_path ) :
os . unlink ( request_path )
with closing ( tempfile . NamedTemporaryFile ( mode = " w " ) ) as request_file :
for src , ver in packages :
if src in self . pkglist and ver in self . pkglist [ src ] :
continue
print ( " %s %s " % ( src , ver ) , file = request_file )
# Update 'pkglist' so even if submit/collect is not called
# (dry-run), britney has some results.
self . pkglist [ src ] [ ver ] = ' RUNNING '
request_file . flush ( )
self . _run ( " request " , " -O " , request_path , request_file . name )
def __init__ ( self , britney , distribution , series , debug = False ) :
def submit ( self ) :
self . britney = britney
""" Submits the current boottests requests for processing. """
self . distribution = distribution
self . _run ( " submit " , self . _request_path )
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 ) :
def collect ( self ) :
""" Return the boottest status for the given excuse.
""" Collects boottests results and updates internal registry. """
self . _run ( " collect " , " -O " , self . _result_path )
self . _read ( )
A new boottest job will be requested if the the source was not
def needs_test ( self , name , version ) :
yet processed , otherwise the status of the corresponding job will
""" Whether or not the given source and version should be tested.
be returned .
Sources are only considered for boottesting if they produce binaries
Sources are only considered for boottesting if they produce binaries
that are part of the phone image manifest . See ` TouchManifest ` .
that are part of the phone image manifest . See ` TouchManifest ` .
"""
"""
# Discover all binaries for the 'excused' source.
# Discover all binaries for the 'excused' source.
unstable_sources = self . britney . sources [ ' unstable ' ]
unstable_sources = self . britney . sources [ ' unstable ' ]
# Dismiss if source is not yet recognized (??).
# Dismiss if source is not yet recognized (??).
if excuse . name not in unstable_sources :
if name not in unstable_sources :
return None
return False
# Binaries are a seq of "<binname>/<arch>" and, practically, boottest
# Binaries are a seq of "<binname>/<arch>" and, practically, boottest
# is only concerned about armhf binaries mentioned in the phone
# is only concerned about armhf binaries mentioned in the phone
# manifest. Anything else should be skipped.
# manifest. Anything else should be skipped.
phone_binaries = [
phone_binaries = [
b for b in unstable_sources [ excuse. name] [ BINARIES ]
b for b in unstable_sources [ name] [ BINARIES ]
if b . split ( ' / ' ) [ 1 ] in self . britney . options . boottest_arches . split ( )
if b . split ( ' / ' ) [ 1 ] in self . britney . options . boottest_arches . split ( )
and b . split ( ' / ' ) [ 0 ] in self . phone_manifest
and b . split ( ' / ' ) [ 0 ] in self . phone_manifest
]
]
return bool ( phone_binaries )
# Process (request or update) a boottest attempt for the source
# if one or more of its binaries are part of the phone image.
if phone_binaries :
status = self . dispatcher . get_status ( excuse . name , excuse . ver [ 1 ] )
else :
status = ' SKIPPED '
return status