#! /usr/bin/env python
#
# Main test driver.

import os
import os.path
import sys
import shutil
import fnmatch
import optparse
import re
import tempfile
import subprocess
import copy
import glob
import fnmatch
import ConfigParser
import time
import multiprocessing
import multiprocessing.managers
import multiprocessing.sharedctypes

VERSION = "0.4" # Automatically filled in.

Name ="btest"
ConfigDefault = "btest.cfg"
Config = None

RE_INPUT = re.compile("%INPUT")
RE_DIR = re.compile("%DIR")
RE_ENV = re.compile("\$\{(\w+)\}")
RE_START_NEXT_TEST = re.compile("@TEST-START-NEXT")
RE_START_FILE = re.compile("@TEST-START-FILE +([^\n ]*)")
RE_END_FILE = re.compile("@TEST-END-FILE")

# Commands as tuple (tag, regexp, more-than-one-is-ok, optional, group-main, group-add)
RE_EXEC          = ("exec",      re.compile("@TEST-EXEC(-FAIL)?: *(.*)"), True, False, 2, 1)
RE_REQUIRES      = ("requires",  re.compile("@TEST-REQUIRES: *(.*)"), True, True, 1, -1)
RE_GROUP         = ("group",     re.compile("@TEST-GROUP: *(.*)"), True, True, 1, -1)
RE_SERIALIZE     = ("serialize", re.compile("@TEST-SERIALIZE: *(.*)"), False, True, 1, -1)
RE_IGNORE_FILTER = ("no-filter", re.compile("@TEST-NO-FILTER: *(.*)"), True, True, 1, -1)
RE_COPY_FILE     = ("copy-file", re.compile("@TEST-COPY-FILE: *(.*)"), True, True, 1, -1)

Commands = (RE_EXEC, RE_REQUIRES, RE_GROUP, RE_SERIALIZE, RE_IGNORE_FILTER, RE_COPY_FILE)

def output(msg, nl=True, file=None):
    if not file:
        file = sys.stderr

    if nl:
        print >>file, msg
    else:
        print >>file, msg,

def warning(msg):
    print >>sys.stderr, "warning:", msg

def error(msg):
    print >>sys.stderr, msg
    sys.exit(1)

def mkdir(dir):
    if not os.path.exists(dir):
        try:
            os.mkdir(dir)
        except OSError, e:
            error("cannot create directory %s: %s" % (dir, e))

    else:
        if not os.path.isdir(dir):
            error("path %s exists but is not a directory" % dir)

def getOption(key, default):
    try:
        return Config.get("btest", key)
    except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
        return default

reBackticks = re.compile(r"`(([^`]|\`)*)`")

def readStateFile():
    try:
        # Read state file.
        tests = []

        for line in open(StateFile):
            line = line.strip()
            if not line or line.startswith("#"):
                continue

            tests += [line]

        tests = findTests(tests, output_handler)

    except IOError:
        return (False, [])

    return (True, tests)

# We monkey-patch the OptionParser to expand backticks.
def cpExpandBackticks(self, section, option, rawval, vars):
    def _exec(m):
        cmd = m.group(1)
        if not cmd:
            return ""

        try:
            return subprocess.Popen(cmd.split(), stdout=subprocess.PIPE).communicate()[0].strip()
        except OSError, e:
            error("cannot execute '%s': %s" % (cmd, e))

    value = cpOriginalInterpolate(self, section, option, rawval, vars)
    value = reBackticks.sub(_exec, value)

    return value

# Replace environment variables in string.
def replaceEnvs(s):
    def replace_with_env(m):
        try:
            return os.environ[m.group(1)]
        except KeyError:
            return ""

    return RE_ENV.sub(replace_with_env, s)

cpOriginalInterpolate = ConfigParser.ConfigParser._interpolate
ConfigParser.ConfigParser._interpolate = cpExpandBackticks

# Main class distributing the work across threads.
class TestManager(multiprocessing.managers.SyncManager):
    def __init__(self):
        super(TestManager, self).__init__()

    def run(self, tests, output_handler):
        self.start()

        self._output_handler = output_handler
        self._lock = self.Lock()
        self._succeeded = multiprocessing.sharedctypes.RawValue('i', 0)
        self._failed = multiprocessing.sharedctypes.RawValue('i', 0)
        self._skipped = multiprocessing.sharedctypes.RawValue('i', 0)
        self._tests = self.list(tests)
        self._failed_tests = self.list([])
        self._num_tests = len(self._tests)

        num_threads = Options.threads

        if num_threads:
            threads = []

            for i in range(num_threads):
                t = multiprocessing.Process(name="#%d" % (i+1), target=lambda : self.threadRun(i))
                t.start()
                threads += [t]

            for t in threads:
                t.join()

        else:
            # No threads, just run all directly.
            self.threadRun(0)

        # Record failed tests if not updating.
        if Options.mode != "UPDATE" and Options.mode != "UPDATE_INTERACTIVE":
            try:
                state = open(StateFile, "w")
            except IOError, e:
                error("cannot open state file %s" % StateFile)

            for t in self._failed_tests:
                print >>state, t

            state.close()

        return (self._succeeded.value, self._failed.value, self._skipped.value)

    def percentage(self):
        if not self._num_tests:
            return 0

        count = self._succeeded.value + self._failed.value + self._skipped.value
        return 100.0 * count / self._num_tests

    def threadRun(self, thread_num):
        while True:
            tests = self.nextTests(thread_num)
            if not tests:
                # No more work for us.
                return

            for t in tests:
                try:
                    t.run(self)
                    self.testReplayOutput(t)
                except KeyboardInterrupt:
                    if Options.threads:
                        # Caught by parent thread.
                        return
                    else:
                        # Rethrow
                        raise


    def nextTests(self, thread_num):
        with self._lock:
            for i in range(len(self._tests)):
                t = self._tests[i]

                if not t:
                    continue

                if Options.threads and t.serialize:
                    if hash(t.serialize) % Options.threads != thread_num:
                        # Not ours.
                        continue

                # We'll execute it, delete from queue.
                del self._tests[i]

                if Options.filters:
                    tests = []

                    for filter in Options.filters:

                        if filter in t.ignore_filters:
                            continue

                        filtered_test = copy.deepcopy(t)

                        if filter == "-":
                            filter = ""

                        filtered_test.setFilter(filter)
                        tests += [filtered_test]

                else:
                    tests = [t]

                return tests

        # No more tests for us.
        return None

    def lock(self):
        return self._lock

    def testStart(self, test):
        with self._lock:
            self._output_handler.testStart(test)

    def testCommand(self, test, cmdline):
        with self._lock:
            self._output_handler.testCommand(test, cmdline)

    def testSucceeded(self, test):
        with self._lock:
            self._output_handler.testSucceeded(test)
            self._succeeded.value += 1

    def testFailed(self, test):
        with self._lock:
            self._output_handler.testFailed(test)
            self._failed.value += 1
            self._failed_tests += [test.name]

    def testSkipped(self, test):
        with self._lock:
            self._output_handler.testSkipped(test)
            self._skipped.value += 1

    def testReplayOutput(self, test):
        with self._lock:
            self._output_handler.replayOutput(test)

# One test.
class Test(object):
    def __init__(self, file, output_handler):
        self.file = file
        self.dir = os.path.abspath(os.path.dirname(file))
        self.basename = None
        self.name = None
        self.number = 1
        self.serialize = None
        self.groups = set()
        self.cmdlines = []
        self.tmpdir = None
        self.diag = None
        self.baseline = None
        self.filter = None
        self.ignore_filters = []
        self.files = []
        self.requires = []
        self.copy_files = []
        self.output_handler = output_handler

    def displayName(self):
        name = self.name

        if self.filter:
            name = "%s [%s]" % (name, self.filter)

        return name

    def setFilter(self, filter):
        self.filter = filter

        # Parse the test's content.
    def parse(self, content):
        cmds = {}
        for line in content:

            if line.find("@TEST-IGNORE") >= 0:
                # Ignore this file.
                return False

            for (tag, regexp, multiple, optional, group1, group2) in Commands:
                m = regexp.search(line)

                if m:
                    value = m.group(group1)

                    if group2 >= 0:
                        value = (value, m.group(group2))

                    if not multiple:
                        if tag in cmds:
                            error("%s: %d defined multiple times." % (test, tag))

                        cmds[tag] = value

                    else:
                        try:
                            cmds[tag] += [value]
                        except KeyError:
                            cmds[tag] = [value]

        # Make sure all non-optional commands are there.
        for (tag, regexp, multiple, optional, group1, group2) in Commands:
            if not optional and not tag in cmds:
                error("%s: mandatory %s command not found." % (self.file, tag))

        (name, ext) = os.path.splitext(self.file)

        self.basename = name.replace("/", ".")
        while self.basename.startswith("."):
            self.basename = self.basename[1:]

        self.name = self.basename
        self.content = content
        self.cmdlines = [(cmd.strip(), success!="-FAIL") for (cmd, success) in cmds["exec"]]

        if "serialize" in cmds:
            self.serialize = cmds["serialize"]

        if "group" in cmds:
            self.groups = set(cmd.strip() for cmd in cmds["group"])

        if "requires" in cmds:
            self.requires = [cmd.strip() for cmd in cmds["requires"]]

        if "copy-file" in cmds:
            self.copy_files = [cmd.strip() for cmd in cmds["copy-file"]]

        if "no-filter" in cmds:
            self.ignore_filters = [cmd.strip() for cmd in cmds["no-filter"]]

        if Substitutions:
            for (key, val) in Substitutions.items():
                self.cmdlines = [(re.sub("\\b" + re.escape(key) + "\\b", val, cmd[0]), cmd[1]) for cmd in self.cmdlines]

        return True

    # Copies all control information over to a new Test but replaces test
    # content with a new one.
    def clone(self, content):
        clone = Test("", self.output_handler)
        clone.file = self.file
        clone.number = self.number + 1
        clone.basename = self.basename
        clone.name = "%s-%d" % (self.basename, clone.number)
        clone.serialize = clone.serialize
        clone.groups = self.groups
        clone.cmdlines = self.cmdlines

        clone.content = content

        return clone

    def run(self, mgr):
        self.mgr = mgr
        mgr.testStart(self)

        self.tmpdir = os.path.abspath(os.path.join(TmpDir, self.name))
        self.diag = os.path.join(self.tmpdir, ".diag")
        self.baseline = os.path.abspath(os.path.join(BaselineDir, self.name))
        self.diagmsgs = []

        self.rmTmp()
        mkdir(self.baseline)
        mkdir(self.tmpdir)

        for (fname, lines) in self.files:
            fname = os.path.join(self.tmpdir, fname)

            subdir = os.path.dirname(fname)
            if subdir != "":
                mkdir(subdir)
            try:
                ffile = open(fname, "w")
            except IOError, e:
                error("cannot write test's additional file '%s'" % fname)

            for line in lines:
                print >>ffile, line,

            ffile.close()

        for file in self.copy_files:
            src = replaceEnvs(file)
            try:
                shutil.copy2(src, self.tmpdir)
            except IOError, e:
                error("cannot copy %s: %s" % (src, e))

        self.localfile = os.path.join(self.tmpdir, os.path.basename(self.file))

        content = open(self.localfile, "w")
        for line in self.content:
            print >>content, line,
        content.close()

        self.log = open(os.path.join(self.tmpdir, ".log"), "w")
        self.stdout = open(os.path.join(self.tmpdir, ".stdout"), "w")
        self.stderr = open(os.path.join(self.tmpdir, ".stderr"), "w")

        for cmd in self.requires:
            (success, rc) = self.execute((cmd, True), apply_filter=self.filter)

            if not success:
                self.mgr.testSkipped(self)
                self.finish()
                return

        failures = 0

        cmds = self.cmdlines

        if Finalizer != "":
            cmds = cmds + [("%s %s" % (Finalizer, self.name), True)]

        for cmd in cmds:

            (success, rc) = self.execute(cmd, apply_filter=self.filter)

            if not success:
                failures += 1

                if failures == 1:
                    self.mgr.testFailed(self)

                if rc == 200:
                    # Abort all tests.
                    sys.exit(1)

                if rc != 100:
                    break

        if failures == 0:
            self.mgr.testSucceeded(self)

            if not Options.tmps:
                self.rmTmp()

        self.finish()

    def finish(self):
        try:
            # Try removing the baseline directory. If it works, it's empty, i.e., no baseline was created.
            os.rmdir(self.baseline)
        except OSError, e:
            pass

        self.log.close()
        self.stdout.close()
        self.stderr.close()

    def execute(self, cmd, apply_filter=None):
        (cmdline, expect_success) = cmd

        # See if we need to apply a filter.
        filter_cmd = None

        if apply_filter:
            try:
                (path, executable) = os.path.split(cmdline.split()[0])
                filter_cmd = Filters[apply_filter][executable]
            except LookupError:
                pass

        if not filter_cmd or not expect_success: # Do not apply filter if we expect failure.
            localfile = self.localfile

        else:
            # This is not quite correct as it does not necessarily need to be
            # the %INPUT file which we are filtering ...
            filtered = os.path.join(self.tmpdir, "filtered-%s" % os.path.basename(self.localfile))
            (success, rc) = self.execute(("%s %s %s" % (filter_cmd, self.localfile, filtered), True), apply_filter=None)
            if not success:
                return (False, rc)

            (success, rc) = self.execute(("mv %s %s" % (filtered, self.localfile), True), apply_filter=None)
            if not success:
                return (False, rc)

            localfile = self.localfile

        self.mgr.testCommand(self, cmdline)

        # Replace special names.
        cmdline = RE_INPUT.sub(localfile, cmdline)
        cmdline = RE_DIR.sub(self.dir, cmdline)

        print >>self.log, cmdline, "(expect %s)" % (("failure", "success")[expect_success])

        env = self.prepareEnv()

        try:
            subprocess.check_call(cmdline, cwd=self.tmpdir, shell=True, env=env, stderr=self.stderr, stdout=self.stdout)
        except subprocess.CalledProcessError, e:
            if expect_success:
                self.diagmsgs += ["'%s' failed unexpectedly (exit code %s)" % (cmdline, e.returncode)]
                return (False, e.returncode)

            else:
                return (True, e.returncode)

        if not expect_success:
            self.diagmsgs += ["'%s' succeeded unexpectedly (exit code 0)" % cmdline]
            return (False, 0)

        return (True, 0)

    def rmTmp(self):
        try:
            if os.path.isfile(self.tmpdir):
                os.remove(self.tmpdir)

            if os.path.isdir(self.tmpdir):
                subprocess.call("rm -rf %s 2>/dev/null" % self.tmpdir, shell=True)

        except OSError, e:
            error("cannot remove tmp directory %s: %s" % (self.tmpdir, e))

    # Prepares the environment for the child processes.
    def prepareEnv(self):
        env = copy.deepcopy(os.environ)
        env["TEST_BASELINE"] = self.baseline
        env["TEST_DIAGNOSTICS"] = self.diag
        env["TEST_MODE"] = Options.mode.upper()
        env["TEST_NAME"] = self.name
        return env

    def addFiles(self, files):
        # files is a list of tuple (fname, lines).
        self.files = files

### Output handlers.

class OutputHandler:
    def __init__(self, options):
        """Base class for reporting progress and results to user. We derive
        several classes from this one, with the one being used depending on
        which output the users wants.

        A handler's method are called from test TestMgr and may be called
        interleaved from different tests. However, the TestMgr locks before
        each call so that it's guaranteed that two calls don't run
        concurrently.

        options: An optparser with the global options.
        """
        self._buffered_output = {}
        self._options = options

    def options(self):
        """Returns the current optparser instance."""
        return self._options

    def threadPrefix(self):
        """In threaded mode, returns a string with the thread's name in a form
        suitable to prefix output with. In non-threaded mode, returns the
        empty string."""
        if self.options().threads:
            return "[%s]" % multiprocessing.current_process().name
        else:
            return ""

    def _output(self, msg, nl=True, file=None):
        if not file:
            file = sys.stderr

        if nl:
            print >>file, msg
        else:
            if msg:
                print >>file, msg,

    def output(self, test, msg, nl=True, file=None):
        """Output one line of output to user. In non-threaded mode, this will
        be printed out directly to stderr. In threaded-mode, this will be
        buffered until the test has finished; then all output is printed as a
        block.

        This should only be called from other members of this class, or
        derived classes, not from tests.
        """
        if not self.options().threads:
            self._output(msg, nl, file)
            return

        else:
            try:
                self._buffered_output[test.name] += [(msg, nl, file)]
            except KeyError:
                self._buffered_output[test.name] = [(msg, nl, file)]

    def replayOutput(self, test):
        """Prints out all output buffered in threaded mode by output()."""
        if not test.name in self._buffered_output:
            return

        for (msg, nl, file) in self._buffered_output[test.name]:
            self._output(msg, nl, file)

        self._buffered_output[test.name] = []

    # Methods to override.
    def testStart(self, test):
        """Called just before a test begins."""
        pass

    def testCommand(self, test, cmdline):
        """Called just before a command line is exected for a trace."""
        pass

    def testSucceeded(self, test):
        """Called when a test was successful."""
        pass

    def testFailed(self, test):
        """Called when a test failed."""
        pass

    def testSkipped(self, test):
        """Called when a test is skipped because its dependencies aren't met."""
        pass

    def finished(self):
        """Called when all tests have been executed."""
        pass

class Forwarder(OutputHandler):
    """
    Forwards output to several other handlers.

    options: An optparser with the global options.

    handlers: List of output handlers to forward to.
    """

    def __init__(self, options, handlers):
        OutputHandler.__init__(self, options)
        self._handlers = handlers

    def testStart(self, test):
        """Called just before a test begins."""
        for h in self._handlers:
            h.testStart(test)

    def testCommand(self, test, cmdline):
        """Called just before a command line is exected for a trace."""
        for h in self._handlers:
            h.testCommand(test, cmdline)

    def testSucceeded(self, test):
        """Called when a test was successful."""
        for h in self._handlers:
            h.testSucceeded(test)

    def testFailed(self, test):
        """Called when a test failed."""
        for h in self._handlers:
            h.testFailed(test)

    def testSkipped(self, test):
        for h in self._handlers:
            h.testSkipped(test)

    def replayOutput(self, test):
        for h in self._handlers:
            h.replayOutput(test)

    def finished(self):
        for h in self._handlers:
            h.finished()

class Standard(OutputHandler):
    def testStart(self, test):
        self.output(test, self.threadPrefix(), nl=False)
        self.output(test, "%s ..." % test.displayName(), nl=False)

    def testCommand(self, test, cmdline):
        pass

    def testSucceeded(self, test):
        self.output(test, "ok")

    def testFailed(self, test):
        self.output(test, "failed")

    def testSkipped(self, test):
        self.output(test, "not available, skipped")

class Console(OutputHandler):
    """Output handler that writes compact progress report to the console."""

    def __init__(self, options):
        OutputHandler.__init__(self, options)
        self.sticky = False

    def testStart(self, test):
        self.consoleOutput(test, "", False)

    def testCommand(self, test, cmdline):
        pass

    def testSucceeded(self, test):
        self.consoleOutput(test, "ok", False)

    def testFailed(self, test):
        self.consoleOutput(test, "failed", True)

    def testSkipped(self, test):
        self.consoleOutput(test, "skipped", False)

    def finished(self):
        sys.stdout.write(chr(27) + '[2K')
        # sys.stdout.write("\r[100%] ")
        sys.stdout.write("\r")
        sys.stdout.flush()

    def consoleOutput(self, test, addl, sticky):
        line = "[%3d%%] %s ..." % (test.mgr.percentage(), test.displayName())

        if addl:
            line += " " + addl

        sys.stdout.write(chr(27) + '[2K')
        sys.stdout.write("\r%s" % line.strip())

        if sticky:
            sys.stdout.write("\n")

        sys.stdout.flush()

class Brief(OutputHandler):
    """Output handler for producing the brief output format."""
    def testStart(self, test):
        pass

    def testCommand(self, test, cmdline):
        pass

    def testSucceeded(self, test):
        pass

    def testFailed(self, test):
        self.output(test, self.threadPrefix(), nl=False)
        self.output(test, "%s ... failed" % test.displayName())

    def testSkipped(self, test):
        pass

class Verbose(OutputHandler):
    """Output handler for producing the verbose output format."""

    def testStart(self, test):
        self.output(test, self.threadPrefix(), nl=False)
        self.output(test, "%s ..." % test.displayName())

    def testCommand(self, test, cmdline):
        self.output(test, self.threadPrefix(), nl=False)
        self.output(test, "  > %s" % cmdline)

    def testSucceeded(self, test):
        self.output(test, self.threadPrefix(), nl=False)
        self.output(test, "... %s ok" % test.displayName())

    def testFailed(self, test):
        self.output(test, self.threadPrefix(), nl=False)
        self.output(test, "... %s failed" % test.displayName())

    def testSkipped(self, test):
        self.output(test, self.threadPrefix(), nl=False)
        self.output(test, "... %s not available, skipped" % test.displayName())

class Diag(OutputHandler):
    def __init__(self, options, all=False, file=None):
        """Output handler for producing the diagnostic output format.

        options: An optparser with the global options.

        all: Print diagnostics also for succeeding tests.

        file: Output into given file rather than console.
        """
        OutputHandler.__init__(self, options)
        self._all = all
        self._file = file

    def showDiag(self, test):
        """Generates diagnostics for a test."""
        for line in test.diagmsgs:
            self.output(test, "  % " + line, True, self._file)

        for f in (test.diag, os.path.join(test.tmpdir, ".stderr")):
            if not f:
                continue

            if os.path.isfile(f):
                self.output(test, "  % cat " + os.path.basename(f), True, self._file)
                for line in open(f):
                    self.output(test, "  " + line.strip(), True, self._file)
                self.output(test, "", True, self._file)

        if self.options().wait and not file:
            self.output(test, "<Enter> ...")
            try:
                sys.stdin.readline()
            except KeyboardInterrupt:
                sys.exit(1)

    def testCommand(self, test, cmdline):
        pass

    def testSucceeded(self, test):
        if self._all:
            if self._file:
                self.output(test, "%s ... ok" % test.displayName(), True, self._file)

            self.showDiag(test)

    def testFailed(self, test):
        if self._file:
            self.output(test, "%s ... failed" % test.displayName(), True, self._file)

        self.showDiag(test)

    def testSkipped(self, test):
        if self._file:
            self.output(test, "%s ... not available, skipped" % test.displayName(), True, self._file)

# Walk the given directory and return all test files.
def findTests(paths, output_handler):
    tests = []

    ignore_files = getOption("IgnoreFiles", "").split()
    ignore_dirs = getOption("IgnoreDirs", "").split()

    for path in paths:
        if os.path.isfile(path):
            tests += readTestFile(path, output_handler)

        elif os.path.isdir(path):
            for (dirpath, dirnames, filenames) in os.walk(path):
                for file in filenames:
                    for glob in ignore_files:
                        if fnmatch.fnmatch(file, glob):
                            break
                    else:
                        tests += readTestFile(os.path.join(dirpath, file), output_handler)

                # Don't recurse into these.
                for skip in ignore_dirs:
                    if skip in dirnames:
                        dirnames.remove(skip)

        else:
            # See if we have a test named like this in our configured set.
            for t in Config.configured_tests:
                if t and path == t.name:
                    tests += [t]
                    break

            else:
                error("cannot read %s" % path)

    return tests

# Read the given test file and instantiate one or more tests from it.
def readTestFile(filename, output_handler):

    def newTest(content, previous):
        if not previous:
            t = Test(filename, output_handler)
            if t.parse(content):
                return t
            else:
                return None
        else:
            return previous.clone(content)

    try:
        input = open(filename)
    except IOError, e:
        error("cannot read test file: %s" % e)

    tests = []
    files = []

    content = []
    previous = None
    file = (None, [])

    state = "test"

    for line in input:

        if state == "test":
            m = RE_START_FILE.search(line)
            if m:
                state = "file"
                file = (m.group(1), [])
                continue

            m = RE_END_FILE.search(line)
            if m:
                error("unexpected @test-end-file")

            m = RE_START_NEXT_TEST.search(line)
            if not m:
                content += [line]
                continue

            t = newTest(content, previous)
            if not t:
                return []

            tests += [t]

            previous = t
            content = []

        elif state == "file":
            m = RE_END_FILE.search(line)
            if m:
                state = "test"
                files += [file]
                file = (None, [])
                continue

            file = (file[0], file[1] + [line])

        else:
            error("internal: unknown state %s" % state)

    if state == "file":
        files += [file]

    tests += [newTest(content, previous)]

    input.close()

    for t in tests:
        if t:
            t.addFiles(files)

    return tests

### Main

optparser = optparse.OptionParser(usage="%prog [options] <directories>", version=VERSION)
optparser.add_option("-U", "--update-baseline", action="store_const", dest="mode", const="UPDATE",
                     help="create a new baseline from the tests' output")
optparser.add_option("-u", "--update-interactive", action="store_const", dest="mode", const="UPDATE_INTERACTIVE",
                     help="interactively asks whether to update baseline for a failed test")
optparser.add_option("-d", "--diagnostics", action="store_true", dest="diag", default=False,
                     help="show diagnostic output for failed tests")
optparser.add_option("-D", "--diagnostics-all", action="store_true", dest="diagall", default=False,
                     help="show diagnostic output for ALL tests")
optparser.add_option("-f", "--file-diagnostics", action="store", type="string", dest="diagfile", default="",
                     help="write diagnostic output for failed tests into file; if file exists, it is overwritten")
optparser.add_option("-v", "--verbose", action="store_true", dest="verbose", default=False,
                     help="show commands as they are executed")
optparser.add_option("-w", "--wait", action="store_true", dest="wait", default=False,
                     help="wait for <enter> after each failed (with -d) or all (with -D) tests")
optparser.add_option("-b", "--brief", action="store_true", dest="brief", default=False,
                     help="outputs only failed tests")
optparser.add_option("-c", "--config", action="store", type="string", dest="config", default=ConfigDefault,
                     help="configuration file")
optparser.add_option("-F", "--filter", action="store", dest="filters", default="",
                     help="activate filter in given config file section")
optparser.add_option("-t", "--tmp-keep", action="store_true", dest="tmps", default=False,
                     help="do not delete tmp files created for running tests")
optparser.add_option("-S", "--subst", action="store", type="string", dest="subst", default=None,
                     help="activate substitution in given config file section")
optparser.add_option("-j", "--jobs", action="store", type="int", dest="threads", default=0,
                     help="number of threads to run tests in simultaneously; 0 disables threading")
optparser.add_option("-g", "--groups", action="store", type="string", dest="groups", default="",
                     help="execute only tests of given comma-separated list of groups")
optparser.add_option("-r", "--rerun", action="store_true", dest="rerun", default=False,
                     help="execute commands for tests that failed last time")
optparser.add_option("-q", "--quiet", action="store_true", dest="quiet", default=False,
                     help="suppress information output other than about failed tests")

optparser.set_defaults(mode="TEST")
(Options, args) = optparser.parse_args()

(basedir, fname) = os.path.split(Options.config)

if not basedir:
    basedir = "."

os.chdir(basedir)

if not os.path.exists(Options.config):
    error("configuration file '%s' not found" % Options.config)

defaults=os.environ
defaults["testbase"] = os.path.abspath(basedir)
defaults["default_path"] = os.environ["PATH"]
Config = ConfigParser.ConfigParser(defaults)
Config.read(Options.config)

if Options.quiet:
    Options.brief = True

# Determine output handlers to use.

output_handlers = []

if Options.verbose:
    output_handlers += [Verbose(Options, )]

elif Options.brief:
    output_handlers += [Brief(Options, )]

else:
    if sys.stdout.isatty():
        output_handlers += [Console(Options, )]
    else:
        output_handlers += [Standard(Options, )]

if Options.diagall:
    output_handlers += [Diag(Options, True, None)]

elif Options.diag:
    output_handlers += [Diag(Options, False, None)]

if Options.diagfile:
    try:
        diagfile = open(Options.diagfile, "w", 1)
        output_handlers += [Diag(Options, Options.diagall, diagfile)]

    except IOError, e:
        print >>sys.stderr, "cannot open %s: %s" (Options.diagfile, e)

output_handler = Forwarder(Options, output_handlers)

# Evaluate other command line options.

if Config.has_section("environment"):
    for (name, value) in Config.items("environment"):
        os.environ[name.upper()] = value

Filters = {}

if Options.filters:
    Options.filters = Options.filters.split(",")

    for filter in Options.filters:

        if filter == "-":
            continue

        Filters[filter] = {}
        sec = "filter-%s" % filter

        if not Config.has_section(sec):
            error("configuration file has no section '%s'" % sec)

        for (name, value) in Config.items(sec):
            Filters[filter][name] = value

Substitutions = {}
if Options.subst:
    sec = "subst-%s" % Options.subst
    if not Config.has_section(sec):
        error("configuration file has no section '%s'" % sec)

    for (name, value) in Config.items(sec):
        Substitutions[name] = value

Config.configured_tests = []

testdirs = getOption("TestDirs", "").split()
if testdirs:
    Config.configured_tests = findTests(testdirs, output_handler)

StateFile = os.path.abspath(getOption("StateFile", os.path.join(defaults["testbase"], ".btest.failed.dat")))
TmpDir = os.path.abspath(getOption("TmpDir", os.path.join(defaults["testbase"], ".tmp")))
BaselineDir = os.path.abspath(getOption("BaselineDir", os.path.join(defaults["testbase"], "Baseline")))
Finalizer = getOption("Finalizer", "")

# if Finalizer:
#    Finalizer = os.path.abspath(Finalizer)

if args:
    tests = findTests(args, output_handler)

else:
    if Options.rerun:
        (success, tests) = readStateFile()

        if success:
            if not tests:
                output("no tests failed last time")
                sys.exit(0)

        else:
            warning("cannot read state file, executing all tests")
            tests = Config.configured_tests

    else:
        tests = Config.configured_tests

if Options.groups:

    Options.groups = set(Options.groups.split(","))

    def rightGroup(t):
        if t.groups & Options.groups:
            return True

        if "-" in Options.groups and not t.groups:
            return True

        return False

    tests = [t for t in tests if rightGroup(t)]

if not tests:
    output("no tests to execute")
    sys.exit(0)

mkdir(BaselineDir)
mkdir(TmpDir)

try:
    (succeeded, failed, skipped) = TestManager().run(copy.deepcopy(tests), output_handler)
    total = succeeded + failed + skipped
except KeyboardInterrupt:
    print >>sys.stderr, "Aborted."
    sys.exit(1)

output_handler.finished()

if failed > 0:
    skipped = (", %d skipped" % skipped) if skipped > 0 else ""
    output("%d of %d test%s failed%s" % (failed, total, "s" if total > 1 else "", skipped))
    sys.exit(1)

else:
    if not Options.quiet:
        output("all %d tests successful" % total)
    sys.exit(0)

