#!/usr/bin/env python
#
# runtest.py - wrapper script for running SCons tests
#
# This script mainly exists to set PYTHONPATH to the right list of
# directories to test the SCons modules.
#
# By default, it directly uses the modules in the local tree:
# ./src/ (source files we ship) and ./etc/ (other modules we don't).
#
# HOWEVER, now that SCons has Repository support, we don't have
# Aegis copy all of the files into the local tree.  So if you're
# using Aegis and want to run tests by hand using this script, you
# must "aecp ." the entire source tree into your local directory
# structure.  When you're done with your change, you can then
# "aecpu -unch ." to un-copy any files that you haven't changed.
#
# When any -p option is specified, this script assumes it's in a
# directory in which a build has been performed, and sets PYTHONPATH
# so that it *only* references the modules that have unpacked from
# the specified built package, to test whether the packages are good.
#
# Options:
#
#	-a		Run all tests; does a virtual 'find' for
#			all SCons tests under the current directory.
#
#	-d		Debug.  Runs the script under the Python
#			debugger (pdb.py) so you don't have to
#			muck with PYTHONPATH yourself.
#
#       -h              Print the help and exit.
#
#       -o file         Print test results to the specified file
#                       in the format expected by aetest(5).  This
#                       is intended for use in the batch_test_command
#                       field in the Aegis project config file.
#
#	-P Python	Use the specified Python interpreter.
#
#	-p package	Test against the specified package.
#
#	-q		Quiet.  By default, runtest.py prints the
#			command line it will execute before
#			executing it.  This suppresses that print.
#
#	-X		The scons "script" is an executable; don't
#			feed it to Python.
#
#       -x scons        The scons script to use for tests.
#
# (Note:  There used to be a -v option that specified the SCons
# version to be tested, when we were installing in a version-specific
# library directory.  If we ever resurrect that as the default, then
# you can find the appropriate code in the 0.04 version of this script,
# rather than reinventing that wheel.)
#

import getopt
import glob
import os
import os.path
import re
import stat
import string
import sys

all = 0
debug = ''
tests = []
printcmd = 1
package = None
scons = None
scons_exec = None
output = None
version = ''

if os.name == 'java':
    python = os.path.join(sys.prefix, 'jython')
else:
    python = sys.executable

cwd = os.getcwd()

if sys.platform == 'win32' or os.name == 'java':
    lib_dir = os.path.join(sys.exec_prefix, "Lib")
else:
    # The hard-coded "python" here is the directory name,
    # not an executable, so it's all right.
    lib_dir = os.path.join(sys.exec_prefix, "lib", "python" + sys.version[0:3])

helpstr = """\
Usage: runtest.py [OPTIONS] [TEST ...]
Options:
  -a, --all                   Run all tests.
  -d, --debug                 Run test scripts under the Python debugger.
  -h, --help                  Print this message and exit.
  -o FILE, --output FILE      Print test results to FILE (Aegis format).
  -P Python                   Use the specified Python interpreter.
  -p PACKAGE, --package PACKAGE
                              Test against the specified PACKAGE:
                                deb           Debian
                                local-tar-gz  .tar.gz standalone package
                                local-zip     .zip standalone package
                                rpm           Red Hat
                                src-tar-gz    .tar.gz source package
                                src-zip       .zip source package
                                tar-gz        .tar.gz distribution
                                zip           .zip distribution
  -q, --quiet                 Don't print the test being executed.
  -v version                  Specify the SCons version.
  -X                          Test script is executable, don't feed to Python.
  -x SCRIPT, --exec SCRIPT    Test SCRIPT.
"""

opts, args = getopt.getopt(sys.argv[1:], "adho:P:p:qv:Xx:",
                            ['all', 'debug', 'help', 'output=',
                             'package=', 'python=', 'quiet',
                             'version=', 'exec='])

for o, a in opts:
    if o == '-a' or o == '--all':
        all = 1
    elif o == '-d' or o == '--debug':
        debug = os.path.join(lib_dir, "pdb.py")
    elif o == '-h' or o == '--help':
        print helpstr
        sys.exit(0)
    elif o == '-o' or o == '--output':
        if not os.path.isabs(a):
            a = os.path.join(cwd, a)
        output = a
    elif o == '-P' or o == '--python':
        python = a
    elif o == '-p' or o == '--package':
        package = a
    elif o == '-q' or o == '--quiet':
        printcmd = 0
    elif o == '-v' or o == '--version':
        version = a
    elif o == '-X':
        scons_exec = 1
    elif o == '-x' or o == '--exec':
        scons = a

def whereis(file):
    for dir in string.split(os.environ['PATH'], os.pathsep):
        f = os.path.join(dir, file)
        if os.path.isfile(f):
            try:
                st = os.stat(f)
            except OSError:
                continue
            if stat.S_IMODE(st[stat.ST_MODE]) & 0111:
                return f
    return None

aegis = whereis('aegis')

sp = []

if aegis:
    paths = os.popen("aesub '$sp' 2>/dev/null", "r").read()[:-1]
    sp.extend(string.split(paths, os.pathsep))
    spe = os.popen("aesub '$spe' 2>/dev/null", "r").read()[:-1]
    spe = string.split(spe, os.pathsep)
else:
    spe = []

sp.append(cwd)

class Test:
    def __init__(self, path, spe=None):
        self.path = path
        self.abspath = os.path.abspath(path)
        if spe:
            for dir in spe:
                f = os.path.join(dir, path)
                if os.path.isfile(f):
                    self.abspath = f
                    break
        self.status = None

if args:
    if spe:
        for a in args:
            if os.path.isabs(a):
                for g in glob.glob(a):
                    tests.append(Test(g))
            else:
                for dir in spe:
                    x = os.path.join(dir, a)
                    globs = glob.glob(x)
                    if globs:
                        for g in globs:
                            tests.append(Test(g))
                        break
    else:
        for a in args:
            for g in glob.glob(a):
                tests.append(Test(g))
elif all:
    tdict = {}

    def find_Test_py(arg, dirname, names, tdict=tdict):
        for n in filter(lambda n: n[-8:] == "Tests.py", names):
            t = os.path.join(dirname, n)
            if not tdict.has_key(t):
                tdict[t] = Test(t)
    os.path.walk('src', find_Test_py, 0)

    def find_py(arg, dirname, names, tdict=tdict):
        for n in filter(lambda n: n[-3:] == ".py", names):
            t = os.path.join(dirname, n)
            if not tdict.has_key(t):
                tdict[t] = Test(t)
    os.path.walk('test', find_py, 0)

    if aegis:
        cmd = "aegis -list -unf pf 2>/dev/null"
        for line in os.popen(cmd, "r").readlines():
            a = string.split(line)
            if a[0] == "test" and not tdict.has_key(a[-1]):
                tdict[a[-1]] = Test(a[-1], spe)
        cmd = "aegis -list -unf cf 2>/dev/null"
        for line in os.popen(cmd, "r").readlines():
            a = string.split(line)
            if a[0] == "test":
                if a[1] == "remove":
                    del tdict[a[-1]]
                elif not tdict.has_key(a[-1]):
                    tdict[a[-1]] = Test(a[-1], spe)

    keys = tdict.keys()
    keys.sort()
    tests = map(tdict.get, keys)
else:
    sys.stderr.write("""\
runtest.py:  No tests were specified on the command line.
             List one or more tests, or use the -a option
             to find and run all tests.
""")


if package:

    dir = {
        'deb'          : 'usr',
        'local-tar-gz' : None,
        'local-zip'    : None,
        'rpm'          : 'usr',
        'src-tar-gz'   : '',
        'src-zip'      : '',
        'tar-gz'       : '',
        'zip'          : '',
    }

    # The hard-coded "python2.1" here is the library directory
    # name on Debian systems, not an executable, so it's all right.
    lib = {
        'deb'        : os.path.join('python2.1', 'site-packages')
    }

    if not dir.has_key(package):
        sys.stderr.write("Unknown package '%s'\n" % package)
        sys.exit(2)

    test_dir = os.path.join(cwd, 'build', 'test-%s' % package)

    if dir[package] is None:
        scons_script_dir = test_dir
        globs = glob.glob(os.path.join(test_dir, 'scons-local-*'))
        if not globs:
            sys.stderr.write("No `scons-local-*' dir in `%s'\n" % test_dir)
            sys.exit(2)
        scons_lib_dir = None
        pythonpath_dir = globs[len(globs)-1]
    elif sys.platform == 'win32':
        scons_script_dir = os.path.join(test_dir, dir[package], 'Scripts')
        scons_lib_dir = os.path.join(test_dir, dir[package])
        pythonpath_dir = scons_lib_dir
    else:
        scons_script_dir = os.path.join(test_dir, dir[package], 'bin')
        l = lib.get(package, 'scons')
        scons_lib_dir = os.path.join(test_dir, dir[package], 'lib', l)
        pythonpath_dir = scons_lib_dir

else:
    sd = None
    ld = None

    # XXX:  Logic like the following will be necessary once
    # we fix runtest.py to run tests within an Aegis change
    # without symlinks back to the baseline(s).
    #
    #if spe:
    #    if not scons:
    #        for dir in spe:
    #            d = os.path.join(dir, 'src', 'script')
    #            f = os.path.join(d, 'scons.py')
    #            if os.path.isfile(f):
    #                sd = d
    #                scons = f
    #    spe = map(lambda x: os.path.join(x, 'src', 'engine'), spe)
    #    ld = string.join(spe, os.pathsep)

    scons_script_dir = sd or os.path.join(cwd, 'src', 'script')

    scons_lib_dir = ld or os.path.join(cwd, 'src', 'engine')

    pythonpath_dir = scons_lib_dir

if scons:
    # Let the version of SCons that the -x option pointed to find
    # its own modules.
    os.environ['SCONS'] = scons
elif scons_lib_dir:
    # Because SCons is really aggressive about finding its modules,
    # it sometimes finds SCons modules elsewhere on the system.
    # This forces SCons to use the modules that are being tested.
    os.environ['SCONS_LIB_DIR'] = scons_lib_dir

if scons_exec:
    os.environ['SCONS_EXEC'] = '1'

os.environ['SCONS_SCRIPT_DIR'] = scons_script_dir
os.environ['SCONS_CWD'] = cwd

os.environ['SCONS_VERSION'] = version

old_pythonpath = os.environ.get('PYTHONPATH')

pythonpaths = [ pythonpath_dir ]
for p in sp:
    pythonpaths.append(os.path.join(p, 'build', 'etc'))
    pythonpaths.append(os.path.join(p, 'etc'))
os.environ['PYTHONPATH'] = string.join(pythonpaths, os.pathsep)

if old_pythonpath:
    os.environ['PYTHONPATH'] = os.environ['PYTHONPATH'] + \
                               os.pathsep + \
                               old_pythonpath

os.chdir(scons_script_dir)

class Unbuffered:
    def __init__(self, file):
        self.file = file
    def write(self, arg):
        self.file.write(arg)
        self.file.flush()
    def __getattr__(self, attr):
        return getattr(self.file, attr)

sys.stdout = Unbuffered(sys.stdout)

for t in tests:
    cmd = string.join([python, debug, t.abspath], " ")
    if printcmd:
        sys.stdout.write(cmd + "\n")
    s = os.system(cmd)
    if s >= 256:
        s = s / 256
    t.status = s
    if s < 0 or s > 2:
        sys.stdout.write("Unexpected exit status %d\n" % s)

fail = filter(lambda t: t.status == 1, tests)
no_result = filter(lambda t: t.status == 2, tests)

if len(tests) != 1:
    if fail:
        if len(fail) == 1:
            sys.stdout.write("\nFailed the following test:\n")
        else:
            sys.stdout.write("\nFailed the following %d tests:\n" % len(fail))
        paths = map(lambda x: x.path, fail)
        sys.stdout.write("\t" + string.join(paths, "\n\t") + "\n")
    if no_result:
        if len(no_result) == 1:
            sys.stdout.write("\nNO RESULT from the following test:\n")
        else:
            sys.stdout.write("\nNO RESULT from the following %d tests:\n" % len(no_result))
        paths = map(lambda x: x.path, no_result)
        sys.stdout.write("\t" + string.join(paths, "\n\t") + "\n")

if output:
    f = open(output, 'w')
    f.write("test_result = [\n")
    for t in tests:
        f.write('    { file_name = "%s";\n' % t.path)
        f.write('      exit_status = %d; },\n' % t.status)
    f.write("];\n")
    f.close()
    sys.exit(0)
else:
    if len(fail):
        sys.exit(1)
    elif len(no_result):
        sys.exit(2)
    else:
        sys.exit(0)