#!/usr/bin/env python
"""
Run the test(s) listed on the command line. If a directory is listed, the script will recursively
walk the directory for files named .mk and run each.

For each test, we run gmake -f test.mk. By default, make must exit with an exit code of 0, and must print 'TEST-PASS'.

Each test is run in an empty directory.

The test file may contain lines at the beginning to alter the default behavior. These are all evaluated as python:

#T commandline: ['extra', 'params', 'here']
#T returncode: 2
#T returncode-on: {'win32': 2}
#T environment: {'VAR': 'VALUE}
#T grep-for: "text"
"""

from subprocess import Popen, PIPE, STDOUT
from optparse import OptionParser
import os, re, sys, shutil, glob

class ParentDict(dict):
    def __init__(self, parent, **kwargs):
        self.d = dict(kwargs)
        self.parent = parent

    def __setitem__(self, k, v):
        self.d[k] = v

    def __getitem__(self, k):
        if k in self.d:
            return self.d[k]

        return self.parent[k]

thisdir = os.path.dirname(os.path.abspath(__file__))

pymake = [sys.executable, os.path.join(os.path.dirname(thisdir), 'make.py')]
manifest = os.path.join(thisdir, 'tests.manifest')

o = OptionParser()
o.add_option('-g', '--gmake',
             dest="gmake", default="gmake")
o.add_option('-d', '--tempdir',
             dest="tempdir", default="_mktests")
opts, args = o.parse_args()

if len(args) == 0:
    args = [thisdir]

makefiles = []
for a in args:
    if os.path.isfile(a):
        makefiles.append(a)
    elif os.path.isdir(a):
        makefiles.extend(glob.glob(os.path.join(a, '*.mk')))

def runTest(makefile, make, logfile, options):
    """
    Given a makefile path, test it with a given `make` and return
    (pass, message).
    """

    if os.path.exists(opts.tempdir): shutil.rmtree(opts.tempdir)
    os.mkdir(opts.tempdir, 0755)

    logfd = open(logfile, 'w')
    p = Popen(make + options['commandline'], stdout=logfd, stderr=STDOUT, env=options['env'])
    logfd.close()
    retcode = p.wait()

    if retcode != options['returncode']:
        return False, "FAIL (returncode=%i)" % retcode
        
    logfd = open(logfile)
    stdout = logfd.read()
    logfd.close()

    if stdout.find('TEST-FAIL') != -1:
        print stdout
        return False, "FAIL (TEST-FAIL printed)"

    if options['grepfor'] and stdout.find(options['grepfor']) == -1:
        print stdout
        return False, "FAIL (%s not in output)" % options['grepfor']

    if options['returncode'] == 0 and stdout.find('TEST-PASS') == -1:
        print stdout
        return False, 'FAIL (No TEST-PASS printed)'

    if options['returncode'] != 0:
        return True, 'PASS (retcode=%s)' % retcode

    return True, 'PASS'

print "%-30s%-28s%-28s" % ("Test:", "gmake:", "pymake:")

gmakefails = 0
pymakefails = 0

tre = re.compile('^#T (gmake |pymake )?([a-z-]+)(?:: (.*))?$')

for makefile in makefiles:
    # For some reason, MAKEFILE_LIST uses native paths in GNU make on Windows
    # (even in MSYS!) so we pass both TESTPATH and NATIVE_TESTPATH
    cline = ['-C', opts.tempdir, '-f', os.path.abspath(makefile), 'TESTPATH=%s' % thisdir.replace('\\','/'), 'NATIVE_TESTPATH=%s' % thisdir]
    if sys.platform == 'win32':
        #XXX: hack so we can specialize the separator character on windows.
        # we really shouldn't need this, but y'know
        cline += ['__WIN32__=1']

    options = {
        'returncode': 0,
        'grepfor': None,
        'env': dict(os.environ),
        'commandline': cline,
        'pass': True,
        'skip': False,
        }

    gmakeoptions = ParentDict(options)
    pymakeoptions = ParentDict(options)

    dmap = {None: options, 'gmake ': gmakeoptions, 'pymake ': pymakeoptions}

    mdata = open(makefile)
    for line in mdata:
        line = line.strip()
        m = tre.search(line)
        if m is None:
            break

        make, key, data = m.group(1, 2, 3)
        d = dmap[make]
        if data is not None:
            data = eval(data)
        if key == 'commandline':
            assert make is None
            d['commandline'].extend(data)
        elif key == 'returncode':
            d['returncode'] = data
        elif key == 'returncode-on':
            if sys.platform in data:
                d['returncode'] = data[sys.platform]
        elif key == 'environment':
            for k, v in data.iteritems():
                d['env'][k] = v
        elif key == 'grep-for':
            d['grepfor'] = data
        elif key == 'fail':
            d['pass'] = False
        elif key == 'skip':
            d['skip'] = True
        else:
            print >>sys.stderr, "%s: Unexpected #T key: %s" % (makefile, key)
            sys.exit(1)

    mdata.close()

    if gmakeoptions['skip']:
        gmakepass, gmakemsg = True, ''
    else:
        gmakepass, gmakemsg = runTest(makefile, [opts.gmake],
                                      makefile + '.gmakelog', gmakeoptions)

    if gmakeoptions['pass']:
        if not gmakepass:
            gmakefails += 1
    else:
        if gmakepass:
            gmakefails += 1
            gmakemsg = "UNEXPECTED PASS"
        else:
            gmakemsg = "KNOWN FAIL"

    if pymakeoptions['skip']:
        pymakepass, pymakemsg = True, ''
    else:
        pymakepass, pymakemsg = runTest(makefile, pymake,
                                        makefile + '.pymakelog', pymakeoptions)

    if pymakeoptions['pass']:
        if not pymakepass:
            pymakefails += 1
    else:
        if pymakepass:
            pymakefails += 1
            pymakemsg = "UNEXPECTED PASS"
        else:
            pymakemsg = "OK (known fail)"

    print "%-30.30s%-28.28s%-28.28s" % (os.path.basename(makefile),
                                        gmakemsg, pymakemsg)

print
print "Summary:"
print "%-30s%-28s%-28s" % ("", "gmake:", "pymake:")

if gmakefails == 0:
    gmakemsg = 'PASS'
else:
    gmakemsg = 'FAIL (%i failures)' % gmakefails

if pymakefails == 0:
    pymakemsg = 'PASS'
else:
    pymakemsg = 'FAIL (%i failures)' % pymakefails

print "%-30.30s%-28.28s%-28.28s" % ('', gmakemsg, pymakemsg)

shutil.rmtree(opts.tempdir)

if gmakefails or pymakefails:
    sys.exit(1)
