#!/usr/bin/python
# -*- mode: python; coding: utf-8 -*-

# atheist
#
# Copyright (C) 2009 David Villa Alises
#
#
# 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 3 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.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA




import os, sys, string
import glob
import subprocess as subp
import optparse
import logging, logging.handlers
import time
import select
import signal
import thread, threading
import copy
import traceback
import tempfile
import types
import re
import commands
from itertools import chain
import inspect
import socket
import ConfigParser
import StringIO
import functools

"""
Verbosity level:

0:
[  OK  ]( 0) samples/test1.test              ls
[ FAIL ]( 2) samples/test1.test              ls na
Total: 0.03s - 1/2

1: INFO
samples/test1.test: 2 tests
[  OK  ]( 0) samples/test1.test              ls
[ FAIL ]( 2) samples/test1.test              ls na
Total: 0.54s - 1/2

2: DEBUG
samples/test1.test loading
samples/test1.test: 2 tests
Executing <Test 'test1.test' - 'ls'>
<Test 'test1.test' - 'ls'> process ends
Executing <Test 'test1.test' - 'ls na'>
<Test 'test1.test' - 'ls na'> process ends
[  OK  ]( 0) samples/test1.test              ls
[ FAIL ]( 2) samples/test1.test              ls na
Total: 0.03s - 1/2
"""

VERSION = '0.'+'$Date: 2009-11-30 02:00:40 +0100 (lun 30 de nov de 2009) $'[7:17].replace('-','')
USAGE = '''%%prog [options] [file|directory]

%%prog is a testing tool.
This is version %s, Copyright (C) 2009 David Villa Alises
%%prog comes with ABSOLUTELY NO WARRANTY; This is free software, and you are
welcome to redistribute it under certain conditions; See COPYING for details.''' % VERSION


# Minimal modificaciton from the standard SMTPHandler from python2.6
class SMTPHandler(logging.Handler):
    """
    A handler class which sends an SMTP email for each logging event.
    """
    def __init__(self, mailhost, fromaddr, toaddrs, subject, credentials=None):
        """
        Initialize the handler.

        Initialize the instance with the from and to addresses and subject
        line of the email. To specify a non-standard SMTP port, use the
        (host, port) tuple format for the mailhost argument. To specify
        authentication credentials, supply a (username, password) tuple
        for the credentials argument.
        """
        logging.Handler.__init__(self)
        if type(mailhost) == types.TupleType:
            self.mailhost, self.mailport = mailhost
        else:
            self.mailhost, self.mailport = mailhost, None
        if type(credentials) == types.TupleType:
            self.username, self.password = credentials
        else:
            self.username = None
        self.fromaddr = fromaddr
        if type(toaddrs) == types.StringType:
            toaddrs = [toaddrs]
        self.toaddrs = toaddrs
        self.subject = subject

    def getSubject(self, record):
        """
        Determine the subject for the email.

        If you want to specify a subject line which is record-dependent,
        override this method.
        """
        return self.subject

    weekdayname = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']

    monthname = [None,
                 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
                 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']

    def date_time(self):
        """
        Return the current date and time formatted for a MIME header.
        Needed for Python 1.5.2 (no email package available)
        """
        year, month, day, hh, mm, ss, wd, y, z = time.gmtime(time.time())
        s = "%s, %02d %3s %4d %02d:%02d:%02d GMT" % (
                self.weekdayname[wd],
                day, self.monthname[month], year,
                hh, mm, ss)
        return s

    def emit(self, record):
        """
        Emit a record.

        Format the record and send it to the specified addressees.
        """
        try:
            import smtplib
            try:
                from email.utils import formatdate
            except ImportError:
                formatdate = self.date_time
            port = self.mailport
            if not port: port = smtplib.SMTP_PORT

            glog.debug("SMTP Connecting: %s:%s" % (self.mailhost, port))

            smtp = smtplib.SMTP(self.mailhost, port)
            msg = self.format(record)
            msg = "From: %s\r\nTo: %s\r\nSubject: %s\r\nDate: %s\r\n\r\n%s" % (
                            self.fromaddr,
                            string.join(self.toaddrs, ","),
                            self.getSubject(record),
                            formatdate(), msg)

            smtp.ehlo()
            smtp.starttls()
            smtp.ehlo()

            if self.username:
                smtp.login(self.username, self.password)
            smtp.sendmail(self.fromaddr, self.toaddrs, msg)
            smtp.close()
        except (KeyboardInterrupt, SystemExit):
            raise
        except:
            self.handleError(record)



class JabberHandler(logging.Handler):

    def __init__(self, account, to_id):

        self.xmpp = __import__('xmpp')
        logging.Handler.__init__(self)

        self.from_id, password = account
        self.to_id = to_id

        jid = self.xmpp.protocol.JID(self.from_id)
        self.user = jid.getNode()
        self.server = jid.getDomain()

        self.log = logging.getLogger("JabberHandler")

        self.log.debug("Connecting %s@%s" % (self.user, self.server))

        self.conn = self.xmpp.Client(self.server, debug=[])
        conres = self.conn.connect()

        if not conres:
            self.log.error("Unable to connect to server %s!" % self.server)
            return

        if conres != 'tls':
            self.log.warning("Unable to estabilish secure connection - TLS failed!")
        authres = self.conn.auth(self.user,
                                 password,
                                 resource=self.server)

        if not authres:
            self.log.error("Unable to authorize on %s - check login/password." % self.server)
            return

        if authres != 'sasl':
            self.log.warning("Unable to perform SASL auth os %s. Old authentication method used!" % self.server)

#        self.conn.sendInitPresence(requestRoster=0)


    def emit(self, record):
#        try:
        mid = self.conn.send(\
            self.xmpp.protocol.Message(to   = self.to_id,
                                  body = self.format(record)))
#        except:
#            self.handleError(record)





class MyFormatter(logging.Formatter):
    def format(self, record):
        record.levelinitial = record.levelname[0]*2
        return logging.Formatter.format(self, record)

LOG_DATEFORMAT = '%d/%m/%y %H:%M:%S'

glog = logging.getLogger('atheist')
glog.setLevel(logging.WARNING)

GREY = chr(27)+'[38m'
RED =   chr(27)+'[31m'
GREEN = chr(27)+'[32m'
IGREEN = chr(27)+'[7;32m'

HIGH = chr(27)+'[1m'
NORM =  chr(27)+'[m'

BOLD = chr(27)+'[1m'


SETUP    = '_setup.test'
TEARDOWN = '_teardown.test'

# FIXME: fichero de configuración .atheist
EXCLUDE_DIRS = ['.svn', '.hg']
EXCLUDE_FILES = [SETUP, TEARDOWN]

abort = False

INCLUDE_RE = re.compile(r"""include\("([\./\w\d]+)"\)\s*$""")


#task status
FAIL = False
OK = True
NOT_EXECUTED = 2
UNKNOWN = 3
NOT_RESULT = None


def beaty_value(value):
    RESULT_STR = {FAIL:         RED   + "FAIL" + NORM,
                  OK:           GREEN + "OK" + NORM,
                  NOT_EXECUTED: GREY  + "skip" + NORM,
                  NOT_RESULT:   GREY  + "--" + NORM,
                  UNKNOWN:      GREY  + "??" + NORM}

    return RESULT_STR[value]

def ellipsis(param, width=30, just=False):
    if not param: param = ''
    cad = str(param).strip()
    retval = cad.split('\n')[0][:width-1]
    retval = unicode(retval, 'utf-8')
    #if retval != cad: retval += "…"
    if retval != cad: retval += "_"
    if just: retval = retval.ljust(width)
    return retval


def compath(path):
    return path.replace(config.base_dir, '.')

def count(n, unit):
    return "%s %s%s" % (n, unit, 's' if n>1 else '')

def unique_filename(prefix=''):
    return "/tmp/atheist_%s_%s_%s" % \
           (prefix, os.getpid(), str(time.time()).replace('.',''))

def run_cmd(cmd):
    ps = subp.Popen(cmd, shell=True,
                    stdout=subp.PIPE, stderr=file('/dev/null', 'w'))
    output = ps.communicate()[0]
    return ps.returncode, output


def file_template():
    return '''# -*- mode:python; coding:utf-8 -*-

# Task(cmd,
#      cwd         = '.',
#      delay       = 0,
#      desc        = 'test template',
#      detach      = False,
#      env         = {},
#      expected    = 0,
#      must_fail   = False,
#      path        = '.',
#      template    = [],
#      timeout     = 5,
#      save_stdout = False,
#      signal      = signal.SIGKILL)
'''


class Record(object):
    "kargs are automatic attributes"

    def __init__(self, **kargs):
        self.__dict__.update(kargs)
        self._attrs = kargs.keys()

    def __getattr__(self, attr):
        if not self.__dict__.has_key(attr):
            raise AttributeError, attr
        return self.__dict__.get(attr, None)

    def __setattr__(self, attr, value):
        self.__dict__[attr] = value

    def __str__(self):
        retval = ""
        for k in self._attrs:
            retval += "%s:'%s' " % (k, self.__dict__[k])

        return "<Record %s>" % retval


class SortedDict(dict):
    '''A fixed-position dictionary. The keys will be stored on a list in the
    same order as are inserted.'''

    def __init__(self, other={}):
        self.__keylist = []
        self.update(other)

    def __getitem__(self, key):
        return dict.__getitem__(self, key)

    def __setitem__(self, key, value):
        dict.__setitem__(self, key, value)
        if key in self.__keylist:
            self.__keylist.remove(key)
        self.__keylist.append(key)

    def update(self, other):
        for k,v in other.items():
            self[k] = v

    def keys(self):
        return self.__keylist

    def values(self):
        return [self[k] for k in self.__keylist]

    def items(self):
        return [(k, self[k]) for k in self.__keylist]

    def __iter__(self):
        return self.__keylist.__iter__()

    def clear(self):
        self.__keylist = []
        dict.clear(self)


class IniParser(ConfigParser.ConfigParser):

    def __init__(self, fname):
        self.fname = fname
        ConfigParser.ConfigParser.__init__(self)
        self.read(fname)


    def get_item(self, path, default=None):
        try:
            if '.' in path:
                sect, name = path.split('.')
            else:
                sect = 'DEFAULT'
                name = key

            retval = self.get(sect, name)

            if not retval.strip():
                glog.warning("File '%s': key '%s' contains nothing" % \
                                 (self.fname, path))

            return retval

        except ConfigParser.NoOptionError, e:
            if default is None: raise
            return default

    def get_sec(self, sec_name):
        try:
            return Record(**dict(self.items(sec_name)))
        except ConfigParser.NoSectionError, e:
            glog.critical("A '%s' section is required in your '%s'." % \
                              (sec_name, self.fname))
            sys.exit(1)

    def __getitem__(self, path):
        try:
            return self.get_item(path)
        except  (ConfigParser.NoOptionError, ConfigParser.NoSectionError), e:
            glog.critical("In file '%s'. %s" % (self.fname, e))
            sys.exit()


class ThreadFunc(threading.Thread):
    def __init__(self, function, args=(), kargs={}, start=True):
        threading.Thread.__init__(self, name=function.__name__)
        self.function = function
        self.args = args
        self.kargs = kargs
        self.active = threading.Event()
        self.active.set()
        if start:
            self.start()

    def cancel(self):
        print '*' * 20
        glog.debug("ThreadFunc.cancel '%s'" % self.function.__name__)
        self.active.clear()

    def run(self):
        self.function(*self.args, **self.kargs)


class ProgressBar:
    def __init__(self, max_val=100, label=''):
        self.max = max_val       # total item amount
        self.label = label       # prefix for the bar
        self.width = 42          # bar width
        self.step = max_val/100
        self.val = 0             # current item amount
        self.blk = 0
        self.pct = 0             # completed percentage

        if config.verbosity or config.quiet or config.stdout or max_val<4:
            self._render = lambda x:None

        self.tinit = time.time()
        self.eta = ''

    def set_width(self, value):
        self.width = value - 7

    def inc(self, val=1, cad=''):
        self.val += val
        self.blk = self.val * self.width / self.max
        self.pct = self.val * 100 / self.max
        if self.val > 3:
            self.eta = "%2.1fs" % (((self.max  - self.val) * (time.time() - self.tinit)) / self.val)
        self._render(cad)

    def _render(self, ustr):
        cad = ':%4s [ %s' % (self.label[:4], self.blk * '#')
        cad += (self.width - self.blk) * '-'
        cad += ' ] %s/%s (%3d%%) %s %s\r' % (str(self.val).rjust(len(str(self.max))),
                                              self.max, self.pct, self.eta,
                                              ellipsis(ustr))
        print cad,
        sys.stdout.flush()

    def clean(self):
        print (' ' * (self.width + 24 + len(self.label))) + '\r',
        sys.stdout.flush()


def print_exc(exc):
    for line in exc.split('\n'):
        if line.strip(): glog.error('| ' + line)


class Public: pass

class Stats(Record):
    def __init__(self):
        self.tini = time.time()
        Record.__init__(self, total=0, ok=0, fail=0)

    def calculate(self, collection):
        tests = [x for x in collection if x.check]
        self.total += len(tests)
        self.ok    += len([x for x in tests if x.result == OK])
        self.fail = self.total - self.ok

    def __str__(self):
        t = "%.2fs" % (time.time() - self.tini)
        if self.ok == self.total:
            return (BOLD + IGREEN + " ALL OK!! " + NORM + " - %s - %s " ) % (t, count(self.total, 'test'))

        return "%s - %s%s%s/%s" % \
            (t,
             HIGH, self.ok, NORM,
             count(self.total, 'test'))

    def ALL(self):
        return self.ok == self.total



class TextTaskRender:

    #FIXME: No debería depender de ConsoleSummaryRender
    @staticmethod
    def render(task):
        retval = ConsoleSummaryRender.task_summary(task)
        for i in [GREEN, RED, NORM, GREY]:
            retval = retval.replace(i,'')
        return retval.strip()



class SummaryRender:
    def __init__(self, name, handler):
        self.log = logging.getLogger(name)
        self.log.addHandler(handler)

    def _make_report(self, task_list):
        retval = '''
## Atheist report ##

SOME TASKS FAILED!

user: %s@%s
date: %s
argv: "%s"

''' % (os.getlogin(), socket.gethostname(),
       time.asctime(), str.join(' ', sys.argv))
        for t in task_list:
            retval += TextTaskRender.render(t) + '\n'

        return retval

    def update(self, task_list):
        glog.debug("%s: sending report to %s" % \
                       (self.__class__.__name__, self.dst))
        self.log.error(self._make_report(task_list))


class ConsoleSummaryRender(SummaryRender):
    "Create a task summary representation for the console"
    def __init__(self):
        self.dst = 'console'
        SummaryRender.__init__(self, "Console",
                               logging.StreamHandler())

    def _make_report(self, TC):
        retval = []
        for taskset in TC.sets:
            retval.append("Task case '%s'" % compath(taskset.fname))
            for task in taskset.tasks:
                retval.append(self.task_summary(task))

        return str.join('\n', retval)

    @classmethod
    def task_summary(self, task):
        retcode = int(task.retcode) if task.retcode != None else '-'

        retval = "  %s(%2s) %s%s%-3d %s | %s" % \
            ("[%s]" % beaty_value(task.result).center(12),
             retcode, task.mode,
             task.initial, task.index,
             ellipsis(task.cmd, 30, True),
             task.desc if task.desc != task.cmd else '')

        if task.conditions_fail:
            for c in task.pre:
                retval += "\n\tpre:  %s" % str(c)
#               if c.value != True: break
            for c in task.post:
                retval += "\n\tpost: %s" % str(c)
#               if c.value != True: break

        return retval


class JabberSummaryRender(SummaryRender):
    def __init__(self, dst):
        self.dst = dst

        handler = JabberHandler(\
            (usercfg['jabber.user'],
             usercfg['jabber.pass']),
            dst)

        SummaryRender.__init__(self,
                              'Jabber-%s' % dst,
                              handler)

    def update(self, TC):
        if not TC.ALL(): SummaryRender.update(self, TC)


class MailSummaryRender(SummaryRender):
    def __init__(self, dst):
        self.dst = dst

        handler = SMTPHandler(\
            (usercfg['smtp.host'],
             usercfg.get_item('smtp.port', 25)),
            usercfg['smtp.user'],
            dst,
            'Atheist report',
            (usercfg['smtp.user'],
             usercfg['smtp.pass']))

        SummaryRender.__init__(self,
                              'Email-%s' % dst,
                              handler)

    def update(self, TC):
        if not TC.ALL(): SummaryRender.update(self, TC)


class TypedList(list):
    def __init__(self, cls):
        self._cls = cls

    def append(self, val):
        glog.error("Use operator += instead")

    def __iadd__(self, val):
        if isinstance(val, list):
            for i in val: self += i
            return self

        assert isinstance(val, self._cls), \
               ("arg must be a '%s'" % self._cls.__name__)
        list.append(self, val)
        return self


class Condition:
    def __init__(self, cads=[]):
        self.value = None
        self._cads  = cads

    def run(self):
        raise NotImplemented

    def evaluate(self):
        self.value = self.run()
        return self.value

#    def expand_vars(self, vars):
#        for c in self._cads:
#            setattr(self, c, string.Template(getattr(self,c)).safe_substitute(vars).strip())

#    def clone(self):
#        retval = copy.copy(self)
#        retval.value = None
#        return retval

    def name(self):
        return self.__class__.__name__

    def __repr__(self):
        return str(self)

    def __str__(self):
        return "<[%s]%s %s>" % \
            (beaty_value(self.value).center(12),
             self.name(), self.basic_info())

    def summary(self):
        return "%s %s>" % (str(self)[:-1], self.more_info())

    def basic_info(self):
        return ''

    def more_info(self):
        return ''


class Callback(Condition, Public):
    def __init__(self, func, args=()):
        self.func = func
        assert isinstance(args, tuple)
        self.args = args
        Condition.__init__(self)

    def run(self):
        try:
            retval = self.func(*self.args)
            if retval is None:
                glog.warning("%s() returns None" % self.func.__name__)
            return bool(retval)

        except Exception, e:
            glog.error("%s: %s" % (self.__class__.__name__, e))
            print_exc(traceback.format_exc())

        return False

    def basic_info(self):
        return "%s: '%s'" % (self.func.__name__, ellipsis(self.args))



class EnvVarDefined(Condition, Public):
    def __init__(self, varname, value=None):
        Condition.__init__(self)
        self.varname = varname
        self.value = value

    def run(self):
        retval = self.varname in os.environ.keys()
        if retval and self.value:
            retval = os.environ[self.varname] == self.value
        return retval

    def basic_info(self):
        return "'%s'" % self.varname


class FileExists(Condition, Public):
    def __init__(self, fname):
        self.fname = fname
        Condition.__init__(self)

    def run(self):
        return os.path.exists(self.fname)

    def basic_info(self):
        return "'%s'" % self.fname


class DirExists(FileExists, Public):
    def run(self):
        return FileExists.run(self) and os.path.isdir(self.fname)


class FileContains(Condition, Public):
    def __init__(self, fname, contains):
        self.fname = fname

        if isinstance(contains, str):
            self.contains = [contains]
        elif isinstance(contains, list):
            assert all([isinstance(x, str) for x in contains]),\
                "FileContains args must be strings"
            self.contains = contains

        Condition.__init__(self)

    def run(self):
        if not os.path.exists(self.fname):
            glog.info("'%s' does not exists" % compath(self.fname))
            return False

        fd = file(self.fname)
        fcontent = fd.read()
        fd.close()
        return all([x in fcontent for x in self.contains])

    def basic_info(self):
        return "'%s' content:'%s'" % (self.fname, ellipsis(self.contains))


class OpenPort(Condition, Public):
    def __init__(self, port, host='localhost', proto='tcp'):
        assert proto in ['tcp','udp']
        assert isinstance(port , int) and 0 < port < 65535
        Condition.__init__(self)

        self.proto = proto
        self.port = port
        self.host = host

    def run(self):
        if self.host == 'localhost':
            self.cmd = 'fuser -n %s %s > /dev/null 2> /dev/null' % \
                       (self.proto, self.port)
        else:
            self.cmd = 'nmap -p%s %s | grep "%s/%s open" > /dev/null' % \
                       (self.port, self.host, self.port, self.proto)
        return os.system(self.cmd) == 0

    def basic_info(self):
        return "'%s/%s'" % (self.proto, self.port)

    def more_info(self):
        return "cmd: %s" % self.cmd


class TaskRunning(Condition, Public):
    def __init__(self, task):
        assert isinstance(task, Task)
        self.task = task
        Condition.__init__(self)

    def run(self):
        return self.task.ps and self.task.ps.poll() is None

    def basic_info(self):
        return "'%s'" % self.task.name


class ProcessRunning(Condition, Public):
    def __init__(self, pid):
        assert isinstance(pid, int)
        self.pid = pid
        Condition.__init__(self)

    def run(self):
        return os.waitpid(self.pid, os.WNOHANG) != (0,0)

    def basic_info(self):
        return "'%s'" % self.pid


class DebPkgInstalled(Condition, Public):
    def __init__(self, package, min_version=None):
        assert isinstance(package, str)
        self.package = package
        self.version = min_version
        Condition.__init__(self)

    def run(self):
        rcode,installed = run_cmd("dpkg -l %s | grep ^ii | awk '{print $3}'" % self.package)


        if rcode != 0: return False

        if self.version is not None:
             return installed >= self.version

        return True

        # FIXME, las dos cosas se pueden hacer en un solo comando
        retval = os.system('dpkg -l %s 2> /dev/null| grep ^ii > /dev/null' % \
                               self.package) == 0

        if retval and self.version:
            installed = commands.getoutput("dpkg -l %s | grep ^ii | awk '{print $3}'" % self.package)

            retval &= installed >= self.version
            glog.debug("%s: pkg:%s req:%s inst:%s" % (self.__class__.__name__, self.package, self.version, installed))

        return retval

    def basic_info(self):
        return "'%s'" % self.package


class AtheistVersion(Condition, Public):
    def __init__(self, version):
        self.version = float(version)
        Condition.__init__(self)

    def run(self):
        glog.debug("%s: current:%s ask:%s" % (self, VERSION, self.version))
        return float(VERSION) >= self.version



class ConditionDecorator(Condition, Public):
    def __init__(self, condition):
        assert isinstance(condition, Condition)
        self.condition = condition
        Condition.__init__(self)

    def expand_vars(self, vars):
        Condition.expand_vars(self.condition, vars)

    def name(self):
        return "%s:%s" % (self.__class__.__name__, self.condition.name())

    def basic_info(self):
        return self.condition.basic_info()

    def more_info(self):
        return self.condition.more_info()


class Not(ConditionDecorator, Public):
    def run(self):
        return not self.condition.run()


class Poll(ConditionDecorator, Public):
    def __init__(self, condition, interval=1, timeout=5):
        ConditionDecorator.__init__(self, condition)
        self.interval = interval
        self.timeout = timeout

    def run(self):
        enlapsed = 0
        while 1:
            glog.debug("Polling condition: %s" % self.condition)
            if self.condition.run(): return True
            if enlapsed > self.timeout: break
            time.sleep(self.interval)
            enlapsed += self.interval

        return False


class RemoteTaskFactory:
    def __init__(self, account):
        self.account = account
        try:
            self.user, self.host = self.account.split('@')
        except ValueError:
            self.host = self.account
            self.user = os.getlogin()

    def __call__(self, cmd, **values):
        values.update({'shell':True})
        return Task('ssh %s "%s"' % (self.account, cmd), **values)



DFL_TIMEOUT = 5
DFL_SIGNAL = signal.SIGKILL


task_attrs = SortedDict({
    'tid':         Record(type_=str,  default=None),
    'check':       Record(type_=bool, default=True),
    'cwd':         Record(type_=str,  default=None),
    'delay':       Record(type_=int,  default=0),
    'desc':        Record(type_=str,  default=''),
    'detach':      Record(type_=bool, default=False),
    'env':         Record(type_=dict, default=os.environ.copy()),
    'expected':    Record(type_=int,  default=0),
    'must_fail':   Record(type_=bool, default=False),
    'path':        Record(type_=str,  default=''),
    'template':    Record(type_=list, default=[]),
    'timeout':     Record(type_=int,  default=DFL_TIMEOUT),
    'save_stdout': Record(type_=bool, default=False),
    'shell':       Record(type_=bool, default=False),
    'signal':      Record(type_=int,  default=DFL_SIGNAL),
    'stdout':      Record(type_=str,  default=''),
    })


class BufFile:

    def __init__(self, fd):
        self.data = ''
        self.fileno = fd.fileno()

    def readline(self):
        new = os.read(self.fileno, 2048)
        if not new:
            yield self.data
            raise StopIteration

        self.data += new
        if not '\n' in self.data:
            yield ''
            raise StopIteration

        for line in self.data.split()[:-1]:
            yield line


class Task(Record, Public):

    @classmethod
    def validate_kargs(cls, _dict):
        for k,v in _dict.items():
            assert task_attrs.has_key(k), \
                "'%s' is not a valid keyword" % k
            assert isinstance(v, task_attrs[k].type_), \
                "'%s' arg type must be %s" % (k, task_attrs[k].type_)
            if v == task_attrs[k].default:
                glog.warning("Default value for '%s' is already '%s'. Remove it." % (k, v))
        return _dict

    def allows(self, kargs, allowed):
        forbidden = set(kargs.keys()) - set(allowed)
        if forbidden:
            glog.critical("You can not use '%s' with %s" % \
            (str.join(' or ', forbidden), self.__class__.__name__))
            sys.exit(1)


    def forbid(self, kargs, forbidden):
        matches = set(forbidden) & set(kargs.keys())
        if matches:
            glog.critical("You can not use '%s' with %s" % \
            (str.join(' or ', matches), self.__class__.__name__))
            sys.exit(1)


    def __init__(self, cmd, **kargs):
        #current_ids = [x.id for x in TC] + [x.id for x in ts]  # All
        current_ids = [x.tid for x in ts]                        # Only this testcase

        assert 'tid' not in kargs.keys() or \
            kargs['tid'] not in current_ids, "Duplicate task id: '%s'" % kargs['tid']

        ts.append(self)
        self.cmd = '' if cmd is None else cmd
        self.kargs = kargs

        defaults = {}
        for k,v in task_attrs.items(): defaults[k] = v.default
        self.__dict__.update(defaults)

        if kargs.has_key('template'):
            for i in kargs['template']:
                self.__dict__.update(Task.validate_kargs(i))

        self.__dict__.update(Task.validate_kargs(kargs))

        # add the user variables to dft environ
        environ = os.environ.copy()
        for k,v in self.env.items():
            environ[k] = string.Template(v).safe_substitute(environ)

        self.env = environ

        self.index = None
        self.fname = ''
        self.gen = TypedList(str)
        self.pre =  TypedList(Condition)
        self.post = TypedList(Condition)
        self.terminates = TypedList(Task)

        assert isinstance(self.cmd, str) or self.cmd is None

        self.mode = None
        self.result = NOT_EXECUTED
        self.conditions_fail = False
        self.ps = None
        self.retcode = None
        self.thread = None

        self.stdout_fd = None
        self.last_task_out = None

        if self.stdout:
            self.stdout_fd = open(self.stdout, 'w')

        if self.save_stdout and not self.stdout:
            fd, self.stdout = tempfile.mkstemp(suffix='.stdout',
                                               prefix='atheist_')
            self.stdout_fd = os.fdopen(fd, 'w')

        if self.stdout:
            self.save_stdout = True
            self.post += FileExists(self.stdout)

        self.index = len(TC) + len(ts) - 1
        self.log = logging.getLogger("%s" % (self.name))
        term = logging.StreamHandler()
        term.setFormatter(MyFormatter(\
                config.timetag + '[%(levelinitial)s] %(name)s: %(message)s',
                LOG_DATEFORMAT))
        term.setLevel(config.loglevel)
        self.log.setLevel(config.loglevel)
        self.log.addHandler(term)


    def kill(self, n):
        try:
            os.killpg(self.ps.pid, n)
        except OSError, e:
            glog.debug("%s" % e)
        except AttributeError:
            self.log.warning("did not even start")
            return

        while 1:
            if self.ps.poll() is not None: return
            time.sleep(0.5)


    @property
    def description(self):
        return  "%s:%s" % (self.name, self.desc)

    def details(self):
        dic = self.__dict__.copy()
        dic['fname'] = compath(dic['fname'])

        keys = ['cmd', 'fname']
        if self.result is not NOT_EXECUTED: keys.append('result')

        # add standard attrs with no-default value
        for k,v in task_attrs.items():
            if getattr(self,k) != v.default:
                keys.append(k)

        attrs = []
        for i in keys:
            attrs.append("%s: '%s'" % (i.rjust(14), dic[i]))

        # multivalued attributes
        for a in ['pre','post','gen','terminates']:
            for i in getattr(self, a):
                attrs.append("%s %s" % ('%s:' % a.rjust(14), i))

        return '%3s:%s\n%s\n' % (self.index, self.__class__.__name__,
                                str.join('\n', attrs))

    def __repr__(self):
        return str(self)

    def __str__(self):
        return "%s: <%s '%s' - '%s'>" % \
               (self.name, self.__class__.__name__,
                os.path.splitext(os.path.basename(self.fname))[0],
                self.cmd.split('\n')[0])

    @property
    def name(self):
        return self.initial + str(self.index)

    @property
    def initial(self):
        retval = self.__class__.__name__[0]
        return retval



#    def expand_vars(self):
#        vars = {
#            'basedir': compath(config.base_dir),
#            'dirname': compath(os.path.dirname(self.fname))}
#
#        def substitute(val):
#             return string.Template(val).safe_substitute(vars).strip()
#
#        if self.cmd:
#            self.rawcmd = self.cmd
#            self.cmd = substitute(self.cmd)
#
#        for p in self.pre + self.post:
#            p.expand_vars(vars)
#
#        for i in range(len(self.gen)):
#            self.gen[i] = substitute(self.gen[i])


    def clean(self):
        "Remove generated files (by explicit declaration)"
        remove = [x for x in self.gen if os.path.exists(x)]
        if not remove: return

        self.log.info("removing generated files")
        for f in remove:
            if os.path.isfile(f):
                self.log.debug("- removing file '%s'" % compath(f))
                os.remove(f)
            elif os.path.isdir(f):
                self.log.warning("- removing directory '%s'" % compath(f))
                if not f.startswith(config.base_dir):
                    self.log.error("cleto-mode disabled. Foreign directories will not be removed")
                    continue

                os.system('rm -r %s' % f)
            else:
                self.log.warning("%s is not a file nor directory!" % compath(f))


    def clone(self):
        d = self.kargs
        d['desc'] = '(copy of %s) %s' % (self.name, d['desc'])
        retval = Task(self.cmd, **d)
        retval.pre = self.pre[:]
        retval.post = self.post[:]
        retval.gen = self.gen[:]
        return retval


    def exec_cmd(self):

        def file_write_tag(text, fd=sys.stdout):
            # FIXME: imprime como lineas textos que no acaban en \n. Necesitamos un readline()
            for line in text.split('\n'):
                if not line: continue
                fd.write("%s| %s\n" % (self.name, line))
                fd.flush()

        def file_write(text, fd=self.stdout_fd):
            fd.write(text)
            fd.flush()


        if not self.cmd: return

        out_funcs = []

        if config.stdout:
            out_funcs.append(file_write_tag)
        elif config.stdout_on_fail:
            self.last_task_out = StringIO.StringIO()
            out_funcs.append(functools.partial(file_write_tag, fd=self.last_task_out))

        if self.stdout:
            out_funcs.append(file_write)

        if self.path:
            os.environ['PATH'] += ':'+self.path

        self.tini = time.time()

        if self.shell:
#            cmd = ('/bin/bash -c \"%s\"' % self.cmd.replace('\"', '\\"')).split()
            cmd = ['/bin/bash', '-c', "%s" % self.cmd]
        else:
            cmd = self.cmd.split()

#        print cmd
#        cmd = 'setsid /bin/sh -c \"%s\"' % self.cmd.replace('\"', '\\"')
#        cmd = self.cmd
        try:
            self.ps = subp.Popen(cmd,
                                 close_fds  = True,
                                 stdin      = subp.PIPE,
                                 stdout     = subp.PIPE,
                                 stderr     = subp.STDOUT,
                                 shell      = False,
                                 bufsize    = 0,
                                 cwd        = self.cwd,
                                 env        = self.env,
                                 preexec_fn = os.setsid)
        except OSError, e:
            self.log.error("%s: '%s'" % (e, self.cmd))
            self.result = FAIL
            return

        self.log.info("starts (pid: %s)" % self.ps.pid)

        tini = time.time()
        read_fds = [self.ps.stdout]
        read_ready = select.select(read_fds,[],[], 0.05)[0]

        while self.ps.poll() is None or read_fds:
            for fd in read_ready:
# http://mail.python.org/pipermail/python-bugs-list/2001-March/004491.html
                time.sleep(0.01)
                data = os.read(fd.fileno(), 2048)
                if not data:
                    read_fds.remove(fd)
                else:
                    for func in out_funcs: func(data)

            read_ready = select.select(read_fds,[],[], 0.2)[0]

            if self.timeout is None: continue

            if time.time()-tini > self.timeout:
                self.log.debug("timeout expired (%.1f>%s), sending signal %s to %s" % \
                                   (time.time() - self.tini, self.timeout,
                                    self.signal, self.ps.pid))
                self.kill(self.signal)


        data = self.ps.stdout.read()
        for func in out_funcs: func(data)

        self.retcode = self.ps.returncode
        self.log.debug("finish with %s" % self.retcode)

        if self.shell and self.retcode == 127:
            self.log.error("No such file or directory: '%s'" % self.cmd)
            self.result = FAIL
            return

        self.result = (self.retcode == self.expected)
        if self.must_fail:
            self.result = self.retcode != 0

        if self.result != OK and self.last_task_out:
            print self.last_task_out.getvalue(),
            sys.stdout.flush()

    #/exec_cmd


class Test(Task):
    def __init__(self, *args, **kargs):
        glog.warning("Test() class is deprecated. Use Task() instead.")
        Task.__init__(self, *args, **kargs)

class TaskFunc(Task):
    def __init__(self, func, args, **kargs):
        assert callable(func)
        self.func = func
        self.args = args

        self.allows(kargs, ['check', 'cwd', 'delay', 'desc',
                            'expected', 'must_fail', 'template',
                            'tid'])

        Task.__init__(self, func.__name__, **kargs)


    def exec_cmd(self):
        current = os.getcwd()
        if self.cwd: os.chdir(self.cwd)

        try:
            self.retcode = apply(self.func, self.args)

            if not isinstance(self.retcode, int):
                glog.error("Function '%s' should return an integer" % self.func.__name__)

            if self.retcode is None:
                self.result = NOT_RESULT
            else:
                self.result = (self.retcode == self.expected)
        except Exception,e:
            self.log.info(e)
            self.result = FAIL

        if self.cwd: os.chdir(current)

        if self.must_fail and self.result == FAIL:
            self.result = OK


class TestFunc(TaskFunc):
    def __init__(self, *args, **kargs):
        glog.warning("TestFunc() class is deprecated. Use TaskFunc() instead.")
        TaskFunc.__init__(self, *args, **kargs)


class Command(Task):
    def __init__(self, *args, **kargs):
        self.forbid(kargs, ['check'])
        Task.__init__(self, *args, **kargs)
        self.check = False


class Daemon(Command):
    def __init__(self, cmd, **kargs):
        self.forbid(kargs, ['check', 'timeout', 'detach', 'expected'])
        Command.__init__(self, cmd, **kargs)
        self.timeout = None
        self.detach = True
        self.expected = -9
        self.check = False


class TaskTerminator(Task):
    def __init__(self, task, **kargs):
        assert isinstance(task, Task)
        self.task = task
        Task.__init__(self, None, **kargs)
        self.desc = "Terminates %s" % task.index
        self.post += Poll(Not(TaskRunning(task)))

    def exec_cmd(self):
        self.log.debug("terminates %s sending signal %s" % \
                           (self.task.name, self.signal))

        Task.kill(self.task, self.task.signal)


class Composite:
    def __init__(self, oper, *tasks):
        self.oper = oper
        self.tasks = tasks

    def exec_cmd(self):
        for t in self.tasks: t.exec_cmd()
        self.result = reduce(self.oper, [t.result for x in self.tasks])


class Template(dict, Public):
    def __init__(self, **kargs):
        self.update(Task.validate_kargs(kargs))


class TaskTemplate(Public):
    def __init__(self, **kargs):
        self.kargs = kargs.copy()

    def __call__(self, cmd, **more):
        this_kargs = self.kargs.copy()
        this_kargs.update(more)
        return Task(cmd, **this_kargs)



def run_test(test):
    if test.detach:
        test.log.debug("detaching")
        test.detach = False
        test.thread = ThreadFunc(run_test, (test,))
        time.sleep(0.2)
        return None

    test.result = NOT_RESULT

    time.sleep(test.delay)

    if test.pre or test.post:
        test.result = OK

    # pre-conditions
    for c in test.pre:
        value = c.evaluate()
        test.log.info("Pre-cond: %s" % c)
        if not value:
            test.conditions_fail = True
            test.result = False
            if not config.keep_going:
                return False

    test.exec_cmd()

    if test.stdout_fd:
        test.stdout_fd.close()

    # post-conditions
    for c in test.post:
        value = c.evaluate()
        test.log.info("Post-cond: %s" % c)
        if not value:
            test.conditions_fail = True
            test.result = False
            if not config.keep_going:
                return False

    # terminates
    for t in test.terminates:
        glog.warning("'terminates' attribute is deprecated. Use TaskTerminator instead.")
        test.log.debug("terminates %s sending signal %s" % \
                           (t.name, t.signal))
        Task.kill(t, t.signal)

    return test.result


class TaskCollection:

    def __init__(self):
        self.sets = []
        self.tini = time.time()
        self.total = 0
        self.ok = 0
        self.fail = 0

    def __len__(self):
        return sum([len(s.tasks) for s in self.sets])

    def append_set(self, fname, tasks):
        self.sets.append(Record(fname=fname, tasks=tasks[:]))

    def __iter__(self):
        "Iterate over all tests"
        for t in chain(*[x.tasks for x in self.sets]):
            yield t

    def calculate(self):
        tests = [x for x in self if x.check]
        self.total += len(tests)
        self.ok    += len([x for x in tests if x.result == OK])
        self.fail = self.total - self.ok

    def __str__(self):
        t = "%.2fs" % (time.time() - self.tini)
        if self.ok == self.total:
            return (BOLD + IGREEN + " ALL OK!! " + NORM + " - %s - %s " ) % (t, count(self.total, 'test'))

        return "%s - %s%s%s/%s" % \
            (t,
             HIGH, self.ok, NORM,
             count(self.total, 'test'))

    def ALL(self):
        return self.ok == self.total


class TaskCase(object):
    def __init__(self, fname, template={}):
        if config.list_only:
            print '-', compath(fname)
            return

        fname = compath(fname)
        self.fname = fname
        aux_fname = os.path.splitext(compath(fname))[0]

        self.template = template
        testdir =  os.path.dirname(fname)
        self.template.update({
                'basedir':  compath(config.base_dir),
                'dirname':  compath(testdir),
                'fname':    aux_fname,
                'testname': os.path.basename(aux_fname)})


        for i in ts[:]: ts.remove(i)  # empty the list :-S

        if not config.skip_setup:
            setup = os.path.join(testdir, SETUP)
            if os.path.exists(setup ):
                self.process_file(setup, task_mode.SETUP)

        self.process_file(fname, task_mode.MAIN)

        if not config.skip_setup:
            teardown = os.path.join(testdir, TEARDOWN)
            if os.path.exists(teardown):
                self.process_file(teardown, task_mode.TEARDOWN)

        for t in ts:
            for i in t.gen:
                t.pre  += Not(FileExists(i))
                t.post += FileExists(i)

        TC.append_set(fname, ts)


        #log.info("%s: %s" % (compath(fname), count(len(ts), 'task')))
        #
        #if not ts:
        #    log.info("%s: does not contains any test" % compath(fname))


    def process_file(self, fname, mode):
        if config.list_only:
            print '-', compath(fname)
            return

        glog.debug("%s loading" % compath(fname))

        # ---
        before = ts[:]
        env = exec_env.copy()
        env['ts'] = ts

        try:
            exec(self.load_file(fname)) in env
        except Exception, e:
            print_exc(traceback.format_exc())
            glog.error("Errors in the test definition '%s'" % fname)
            sys.exit(1)

        for t in [x for x in ts if x not in before]:
            t.fname = fname
        # ---

        for t in ts:
            if t.mode is None: t.mode = mode


    def load_file(self, fname):
        def include_error(n, line):
            glog.error("Syntax error in: '%s:%s'" % (fname, n+1))
            glog.error("               : %s" % line)

        lines = [string.Template(x).safe_substitute(self.template) \
                     for x in file(fname).readlines()]

        for n, line in enumerate(lines):
            if not line.startswith('include('): continue

            #print '-', line
            index = lines.index(line)
            #print index
            lines.remove(line)

            try:
                line = line.strip()
                included_file = INCLUDE_RE.match(line).group(1)
            except AttributeError:
                include_error(n, line)
                sys.exit(1)

            try:
                glog.debug("included file: '%s'" % included_file)
                ilines = file(included_file).readlines()
                lines[index:index] = ilines
            except IOError, e:
                include_error(n, line)
                glog.error(e)
                sys.exit(1)

        return str.join('', lines)


#class TaskSet:
#
#    def __init__(self):
#        self.files = []
#
#    def append_test(self, fname, task):
#        if not self.files or self.files[-1].fname != fname:
#            self.current_file = Record(fname=fname, tasks=[])
#            self.files.append(self.current_file)
#
#        self.current_file.tasks.append(task)
#
#    def __len__(self):
#        return sum([len(f.tasks) for f in self.files])
#
#    def __iter__(self):
#        for t in chain(*[f.tasks for f in self.files]):
#            yield t

TC = TaskCollection()
ts = [] # cargados en cada fichero

def get_task(_id):
    for t in chain(ts):
        if t.tid == _id: return t
    glog.error("There is no task with id '%s'" % _id)
    return None


def process_script(code, testset, template={}, fname=''):

    before = testset[:]

    template.update({
        'basedir': compath(config.base_dir)})

    env = exec_env.copy()
    env['ts'] = testset

    code = string.Template(code).safe_substitute(template)

    try:
        exec(code) in env
    except Exception, e:
        glog.error("Errors in the test definition '%s'" % fname)
        print_exc(traceback.format_exc())
        sys.exit(1)

    for t in [x for x in testset if x not in before]:
        t.fname = fname


class task_mode:
    MAIN,SETUP,TEARDOWN = ['-', 's', 't']


def process_directory(dname):
    for root, dirs, files in os.walk(dname):
        glog.info("Entering directory '%s'" % compath(root))
        for f in sorted([x for x in files if x.endswith(config.ext)]):
            if f in EXCLUDE_FILES: continue
            TaskCase(os.path.join(root, f))
        for d in EXCLUDE_DIRS:
            if d in dirs: dirs.remove(d)


def main():

    if config.inline:
        print "inline script:", config.inline
        fdn, fname = tempfile.mkstemp(suffix='.test', prefix='atheist_')
        fd = os.fdopen(fdn, 'w')
        fd.write(config.inline)
        fd.close()
        TaskCase(fname)
        #process_file(fname)


    for i in sorted([os.path.abspath(x) for x in args]):
        if os.path.isdir(i):
            process_directory(i)

        elif os.path.isfile(i):
            if i in [SETUP, TEARDOWN]: continue
            TaskCase(i)

        else:
            glog.warning("Could not read file '%s'" % compath(i))
            return 1


    if config.describe:
        for t in TC: print t.details()
        return


    if config.clean_only:
        for t in TC:
            t.clean()
        return


    pb = ProgressBar(len(TC), 'Run ')
    pb.set_width(int(os.environ.get('COLUMNS', 50)))
    for f in TC.sets:
        glog.info("Task case %s" % (compath(f.fname)+' ').ljust(80, '-'))
        for t in f.tasks:
            if abort: break

            glog.info(t)
            result = run_test(t)

            if result == False and t.check == True \
                    and not config.keep_going:
                t.log.info("FAIL, skipping remaining tasks ('keep-going mode' disabled)")
                break

            pb.inc() #pb.inc(1, t.description)

        for t in f.tasks:
            if config.clean: t.clean()

        while 1:
            unfinished = [x for x in f.tasks if x.thread \
                              and x.thread.isAlive() \
                              and x.timeout is not None]

            if not unfinished: break

            for t in unfinished:
                t.log.debug("waiting to finish detached")

                if abort:
                    Task.kill(t, signal.SIGKILL)

                t.thread.join(1)

        daemons = [x for x in f.tasks if x.thread and x.thread.isAlive() \
                       and x.timeout is None]
        if daemons:
            glog.debug("-- Killing unterminated daemons...")
            for t in daemons:
                glog.debug("sending signal %s to %s" % (t.signal, t.name))
                Task.kill(t, t.signal)

                t.thread.join(1)

    pb.clean()

    TC.calculate()

    notifiers = []
    if not config.quiet:
        notifiers.append(ConsoleSummaryRender())

    if config.jabber:
        for i in config.jabber:
            notifiers.append(JabberSummaryRender(i))
    for i in config.mail:
        notifiers.append(MailSummaryRender(i))

    for n in notifiers:
        n.update(TC)

    if TC.total:
        print "Total:", TC

    return TC.fail


def abort_handler(sign, frame):
    global abort
    print
    glog.warning("C-c pressed!")
    abort = True



def Atheist_env(names):

    def include(val, env):
        env[val.__name__] = val

    retval = {}
    for n in names:
        exec("c = %s" % n)
        if inspect.isclass(c) and issubclass(c, Public):
            include(c, retval)

    include(get_task, retval)

    return retval


exec_env = Atheist_env(dir())


if __name__ == '__main__':
    signal.signal(signal.SIGPIPE, signal.SIG_DFL)
    signal.signal(signal.SIGINT, abort_handler)
    signal.signal(signal.SIGTERM, abort_handler)

    parser = optparse.OptionParser(usage=USAGE)

    parser.add_option('-a', '--test-args', dest='test_args',
                      default='',
                      help='colon-separated options for the tests')

    parser.add_option('-b', '--base-dir', dest='base_dir',
                      default=os.getcwd(),
                      help='set working directory')

    parser.add_option('-C', '--clean-only', dest='clean_only',
                      action='store_true',
                      help="Execute nothing, only remove generated files")

    parser.add_option('-d', '--describe', dest='describe',
                      action='store_true',
                      help='describe tasks but execute nothing')

    parser.add_option('--dirty', dest='clean',
                      action='store_false', default=True,
                      help="'don't remove generated files after test execution")

    parser.add_option('-e', '--stderr', dest='stderr',
                      action='store_true',
                      help='print test process stderr')

    parser.add_option('-f', '--stdout-on-fail', dest='stdout_on_fail',
                      action='store_true',
                      help='print task output but only if it fail')

    parser.add_option('-g', '--gen-template', dest='gen_template',
                      action='store_true',
                      help='generate a test file template with default values')

    parser.add_option('-j', '--skip-setup', dest='skip_setup',
                      action='store_true',
                      help='skip _setup and _teardown files')

    parser.add_option('-k', '--keep-going', dest='keep_going',
                      action='store_true', default=False,
                      help='continue even with failed tests')

    parser.add_option('-l', '--list', dest='list_only',
                      action='store_true',
                      help='list tests but do not execute them')

    parser.add_option('--notify-smtp', dest='mail',
                      action='append', default=[],
                      help='notify failed tests to the given email address')

    parser.add_option('--notify-jabber', dest='jabber',
                      action='append', default=[],
                      help='notify failed tests to the given jabber account')

    parser.add_option('-o', '--stdout', dest='stdout',
                      action='store_true',
                      help='print test process stdout')

    parser.add_option('-q', '--quiet', dest='quiet',
                 action='store_true',
                 help='do not show result summary nor warnings, only totals')

    parser.add_option('-s', '--script', dest='inline',
                      help='specifies command line script')

    parser.add_option('-t', '--time-tag', dest='timetag',
                      action='store_true',
                      help='include time info in the logs')

    parser.add_option('-v', '--verbose', dest='verbosity',
                      action='count', default=0,
                      help='incresse verbosity')

    parser.add_option('-x', '--extension', dest='ext',
                      default='.test',
                      help='file extension for the test files')

    (config, args) = parser.parse_args()

    # FIXME:probablemente optparse tiene soporte para esto
    if config.stdout and config.stdout_on_fail:
        logging.error("-o and -f are incompatible options")
        sys.exit(1)

    if config.verbosity and config.quiet:
        logging.error("-q and -v are incompatible options")
        sys.exit(1)

    usercfg = IniParser(os.path.join(os.environ['HOME'], '.atheist'))

    if config.gen_template:
        print file_template()
        sys.exit(0)

    if not args and not config.inline:
        parser.print_help()
        sys.exit(1)

    config.loglevel = logging.WARNING
    if config.verbosity == 1:
        config.loglevel = logging.INFO
    elif config.verbosity >= 2:
        config.loglevel = logging.DEBUG
    if config.quiet:
        config.loglevel = logging.ERROR

    glog.setLevel(config.loglevel)

    config.timetag = "[%(asctime)s]" if config.timetag else ''

    console = logging.StreamHandler()
    console.setFormatter(MyFormatter(\
            config.timetag + '[%(levelinitial)s] %(message)s',
            LOG_DATEFORMAT))
    glog.addHandler(console)

    exec_env['args'] = config.test_args.split(',')

    sys.exit(main())

