view bin/gub-tester @ 3455:f50975335ad8

Support testing user-checked-out trees again; 3rd option in gub-tester help. Oops, actually remove old test-gub.
author Jan Nieuwenhuizen <janneke@gnu.org>
date Wed, 09 May 2007 17:02:27 +0200
parents 07510997bd2f
children 3d758d262724
line wrap: on
line source

#!/usr/bin/python

def argv0_relocation ():
    import os, sys
    bindir = os.path.dirname (sys.argv[0])
    prefix = os.path.dirname (bindir)
    sys.path.insert (0, prefix)

argv0_relocation ()

import sys
import re
import os
import smtplib
import email.MIMEText
import email.Message
import email.MIMEMultipart
import optparse
import time
import dbhash


from gub import repository
from gub import oslog


################################################################
# utils.

def system (c):
    print c
    if os.system (c):
        raise Exception ('barf')
    
    
def read_tail (file, amount=10240):
    f = open (file)
    f.seek (0, 2)
    length = f.tell()
    f.seek (- min (length, amount), 1)
    return f.read ()

def canonicalize_string (target):
    canonicalize = re.sub ('[ \t\n]', '_', target)
    canonicalize = re.sub ('[^a-zA-Z0-9-]+', '_', canonicalize)
    return canonicalize

log_file = None

################################################################
#

def result_message (parts, subject='') :
    """Concatenate PARTS to a Message object."""
    
    if not parts:
        parts.append ('(empty)')
    
    parts = [email.MIMEText.MIMEText (p) for p in parts if p]

    msg = parts[0]
    if len (parts) > 1:
        msg = email.MIMEMultipart.MIMEMultipart ()
        for p in parts:
            msg.attach (p)
    
    msg['Subject'] = subject
    msg.epilogue = ''

    return msg

def opt_parser ():
    if os.environ.has_key ('EMAIL'):
        address = os.environ['EMAIL']
    else:
        try:
            address = '%s@localhost' % os.getlogin ()
        except OSError:
            address = 'root@localhost'

    p = optparse.OptionParser (usage='gub-tester [options] command command ... ')
    p.address = [address]
    
    examples = '''Examples:
gub-tester --repository=downloads/lilypond.git \\
    gub --branch=lilypond:master lilypond

gub-tester --url=http://bazaar.launchpad.net/~yaffut/yaffut/yaffut.bzr \\
    'make all check'

bzr branch http://bazaar.launchpad.net/~yaffut/yaffut/yaffut.bzr
cd yaffut.bzr && gub-tester 'make all check'

'''

    def format_examples (self):
        return examples
    if p.__dict__.has_key ('epilog'):
        p.formatter.format_epilog = format_examples
        p.epilog = examples
    else:
        p.formatter.format_description = format_examples
        p.description = examples
    p.add_option ('-t', '--to',
                  action='append',
                  dest='address',
                  default=[],
                  help='where to send error report')
    p.add_option ('--dry-run',
                  dest='dry_run',
                  default=False,
                  action='store_true',
                  help='do not run any commands')
    
    p.add_option ('--append-diff',
                  dest='append_diff',
                  default=False,
                  action='store_true',
                  help='append diff since last successful run')
    
    p.add_option ('--bcc',
                  action='append',
                  dest='bcc_address',
                  default=[],
                  help='BCC for error report')

    p.add_option ('-f', '--from',
                  action='store',
                  dest='sender',
                  default=address,
                  help='whom to list as sender')

    p.add_option ('--branch',
                  action='store',
                  dest='branch',
                  default='HEAD',
                  help='which branch to fetch [GIT repositories]')
    
    p.add_option ('--url',
                  action='store',
                  dest='url',
                  default=None,
                  help='where to fetch sources')
    
    p.add_option ('--update',
                  action='store_true',
                  dest='update',
                  default=False,
                  help='checkout or update sources')
    
    p.add_option ('--revision',
                  action='store',
                  dest='revision',
                  default=None,
                  help='what revision to fetch')
    
    p.add_option ('--repository',
                  action='store',
                  dest='repository',
                  default='.',
                  help='where to download/cache repository')
    
    p.add_option ('--tag-repo',
                  action='store',
                  dest='tag_repo',
                  default='',
                  help='where to push success tags.')

    p.add_option ('--quiet',
                  action='store_true',
                  dest='be_quiet',
                  default=False,
                  help='only send mail when there was an error.')
    
    p.add_option ('--dependent',
                  action='store_true',
                  dest='is_dependent',
                  default=False,
                  help='test targets depend on each other')
                  
    p.add_option ('--posthook',
                  action='append',
                  dest='posthooks',
                  default=[],
                  help='commands to execute after successful tests.')

    p.add_option ('--test-self',
                  action='store_true',
                  dest='test_self',
                  default=False,
                  help='run a cursory self test.')

    p.add_option ('-s', '--smtp',
                  action='store',
                  dest='smtp',
                  default='localhost',
                  help='SMTP server to use.')

    p.add_option ('--result-directory',
                  action='store',
                  dest='result_dir',
                  help='Where to store databases test results',
                  default='log')
                  
    p.add_option ('-v', '--verbose', action='count', dest='verbose', default=0)

    return p


def get_db (options, name):
    name = options.result_dir + '/%s.db' % name

    db_file = os.path.join (options.result_dir, name)
    db = dbhash.open (db_file, 'c')
    return db
                        

def test_target (repo, options, target, last_patch):
    canonicalize = canonicalize_string (target)
    release_hash = repo.get_checksum ()

    done_db = get_db (options, canonicalize)
    if done_db.has_key (release_hash):
        log_file.info ('release %(release_hash)s has already been checked'
                      % locals ())
        return None
    
    logfile = 'test-%(canonicalize)s.log' %  locals ()
    logfile = os.path.join (options.result_dir, logfile)
    
    cmd = 'nice time sh -c "(%(target)s)" > %(logfile)s 2>&1' %  locals ()

    log_file.command (cmd)

    if options.dry_run:
        return ('SUCCESS', ['dryrun'])
        
    stat = os.system (cmd)
    base_tag = 'success-%(canonicalize)s-' % locals ()
    result = 'unknown'
    attachments = []

    body = read_tail (logfile, 10240).split ('\n')
    if stat:
        result = 'FAIL'
        attachments = ['error for\n\n\t%s\n\n\n%s' % (target,
                                               '\n'.join (body[-0:]))]
        if options.append_diff:
            attachments += [repo.get_diff_from_tag (base_tag)]
    else:
        if 0:
            # FIXME: find in archives, what did this do?
            # format of last_patch?
            # canonicalize_target vs canonicalize_string?
            print last_patch
            tag = base_tag + canonicalize_string (last_patch['date'])
            repo.tag (tag)
            log_file.log ('tagging with %s' % tag)
            if options.tag_repo:
                repo.push (tag, options.tag_repo)

        result = 'SUCCESS'
        attachments = ['success for\n\n\t%s\n\n%s'
                       % (target,
                          '\n'.join (body[-10:]))]

    log_file.info ('%s: %s' % (target, result))
    
    done_db[release_hash] = time.ctime ()
    return (result, attachments)
    
def send_message (options, msg):
    if not options.address:
        log_file.info ('No recipients for result mail')
        # FIXME: what about env[EMAIL]?
        return
    
    COMMASPACE = ', '
    msg['From'] = options.sender
    msg['To'] = COMMASPACE.join (options.address)
    if options.bcc_address:
        msg['BCC'] = COMMASPACE.join (options.bcc_address)
        
    msg['X-Autogenerated'] = 'lilypond'
    connection = smtplib.SMTP (options.smtp)
    connection.sendmail (options.sender, options.address, msg.as_string ())


def send_result_by_mail (options, parts, subject='Autotester result'):
    msg = result_message (parts, subject)
    send_message (options, msg)

def print_results (options, parts, subject='Autotester result'):
    print '\n===\n\nSUBJECT: ', subject
    print '\n---\n'.join (parts)
    print 'END RESULT\n==='

def real_main (options, args, handle_result):
    global log_file

    log = 'log/gub-tester.log'
    if options.dry_run:
        log = '/dev/stdout'

    log_file = oslog.Os_commands (log, options.verbose)
    
    log_file.info (' *** %s' % time.ctime ())
    log_file.info (' *** Starting tests:\n  %s' % '\n  '.join (args))

    repo = repository.get_repository_proxy (options.repository,
                                            options.url,
                                            options.revision,
                                            options.branch)

    if (not repo.is_downloaded () or options.update or options.revision):
        repo.download ()
        repo.update_workdir ('.')

    log_file.info ('Repository %s' % str (repo))
    
    last_patch = repo.get_revision_description ()
    release_hash = repo.get_checksum ()

    release_id = '''

Last patch of this release:

%(last_patch)s\n

Checksum of revision: %(release_hash)s

''' % locals ()


    summary_body = '\n\n'
    results = {}
    failures = 0
    for a in args:
        result_tup = test_target (repo, options, a, last_patch)
        if not result_tup:
            continue
        (result, atts) = result_tup
        results[a] = result_tup
        success = result.startswith ('SUCCESS')
        if not (options.be_quiet and success):
            handle_result (options, atts, subject='Autotester: %s %s'
                           % (result, a))

        summary_body += '%s\n  %s\n'  % (a, result)

        if not success:
            failures += 1
            if options.is_dependent:
                break

    if (results
        and len (args) > 1
        and (failures > 0 or not options.be_quiet)):
        
        handle_result (options,
                       [summary_body, release_id],
                       subject='Autotester: summary')

    if failures == 0 and results:
        for p in options.posthooks:
            os.system (p)

def test_self (options, args):
    self_test_dir = 'test-gub-test.darcs'
    system ('rm -rf %s ' %  self_test_dir)
    system ('mkdir %s ' %  self_test_dir)
    os.chdir (self_test_dir)
    system ('mkdir log')
    system ('''echo '#!/bin/sh
true' > foo.sh''')
    system ('darcs init')
    system ('echo author > _darcs/prefs/author')
    system ('darcs add foo.sh')
    system ('darcs record -am "add bla"')
    options.repository = os.getcwd ()
    real_main (options, ['false', 'true', 'sh foo.sh'], print_results)

    system ('''echo '#!/bin/sh
true' > foo.sh''')
    system ('darcs record  -am "change bla"')
    real_main (options, ['sh foo.sh'], print_results)
    
def main ():
    (options, args) = opt_parser ().parse_args ()

    if not os.path.isdir (options.result_dir):
        os.makedirs (options.result_dir)

    options.result_dir = os.path.abspath (options.result_dir)
    
    if options.test_self:
        test_self (options, args)
    else:
        real_main (options, args, send_result_by_mail)

if __name__ == '__main__':    
    main ()