#!/usr/bin/env python

# Test compiler against suite of Python programs.  Requires that pyunparse be
# on the path.

import os, sys, re
from subprocess import Popen, PIPE
from getopt import getopt, GetoptError

def Usage():
    print '''
Usage: test-proj1 [ --errors ] [ --compiler=COMPILER ] [ --retain ] DIR/BASE.py ...
    Runs COMPILER (default ./apyc) on each DIR/BASE.py with the command
        COMPILER --stage=1 -o BASE.ast PROGRAM-FILE
    In the absence of the --errors option, unparses the result into
    a Python program BASE-2.py, and then runs python on that program.
    If there is a file DIR/BASE.in, uses that as the standard input.  
    Otherwise, uses the empty file for the standard input.  Compares 
    the output to file DIR/BASE.std, if that is present, and otherwise
    just checks that the python interpreter exits normally.  Retains ASTs
    if --retain.

    With the --errors option, checks that the compiler exits with a
    non-zero exit code and that stderr contains at least one error message
    in the correct format.  Does not unparse the resulting AST or execute
    the Python interpreter.

    Reports results and exits with 0 if there are no errors, and
    with 1 otherwise.'''

def Run(command, *args, **keys):
    if args:
        command = command % args
    proc = Popen (command, shell=True, stdin=PIPE, stdout=PIPE, stderr=PIPE)
    out, err = proc.communicate(keys.get ('stdin', ''))
    return proc.returncode, out, err

def Remove(file):
    try:
        os.unlink(file)
    except OSError:
        pass

def Contents(file):
    try: 
        f = open(file)
        r = f.read()
        f.close()
        return r
    except:
        return ""

def Compile(prog, ast):
    global Stdout, Stderr
    code, Stdout, Stderr = Run("%s --phase=1 -o %s %s", compiler, ast, prog)
    return code == 0

def Unparse(ast, python):
    code, out, err = Run("pyunparse -o %s --remove-extensions %s", python, ast)
    return code == 0

def Execute(prog, inp):
    global Stdout, Stderr
    code, Stdout, Stderr = Run("python %s", prog, stdin=inp)
    return code == 0

def HasError(errout):
    return re.search (r'(?m)^[^:]+\.py:\d+:\s*\S', errout)

try:
    opts, tests = getopt (sys.argv[1:], 'h',
                          ['help', 'retain', 'errors', 'compiler='])
except GetoptError:
    Usage()
    sys.exit(1)

compiler = './apyc'
errors = False
retain = False

for opt, val in opts:
    if opt in ( '-h', '--help' ):
        Usage()
        sys.exit(0)
    elif opt == '--errors':
        errors = True
    elif opt == '--compiler':
        compiler = val
    elif opt == '--retain':
        retain = True

N = 0
OK = 0

for f in tests:
    N += 1
    dir, file = os.path.split(f)
    base, ext = os.path.splitext(file)
    print base + ".py:",

    ast = base + ".ast"
    if errors:
        if Compile(f, ast):
            msg = "FAIL (wrong exit code)"
        elif HasError (Stderr):
            msg = "OK"
        else:
            msg = "FAIL (bad error message)"
    else:
        prog2 = base + "-2.py"
        inp = os.path.join(dir, base + ".in")
        std = Contents (os.path.join(dir, base + ".std"))
        if not Compile(f, ast):
            msg = "FAIL (wrong exit code)"
        elif Stderr:
            msg = "FAIL (error messages)"
        elif not Unparse(ast, prog2):
            msg = "FAIL (bad AST)"
        elif not Execute(prog2, Contents(inp)):
            msg = "FAIL (execution error)"
        elif Stderr:
            msg = "FAIL (error output on execution)"
        elif std and std != Stdout:
            msg = "FAIL (wrong output)"
        else:
            msg = "OK"
        Remove(prog2)
    if not retain:
        Remove(ast)

    if msg == "OK":
        OK += 1
    print msg

print
print "Ran %d tests." % N,
if OK == N:
    print "All passed."
    sys.exit(0)
else:
    print "%d failed." % (N - OK)
    sys.exit(1)

