#!/usr/bin/env python
#     Copyright 2012, Kay Hayen, mailto:kayhayen@gmx.de
#
#     Part of "Nuitka", an optimizing Python compiler that is compatible and
#     integrates with CPython, but also works on its own.
#
#     If you submit patches or make the software available to licensors of
#     this software in either form, you automatically them grant them a
#     license for your part of the code under "Apache License 2.0" unless you
#     choose to remove this notice.
#
#     Kay Hayen uses the right to license his code under only GPL version 3,
#     to discourage a fork of Nuitka before it is "finished". He will later
#     make a new "Nuitka" release fully under "Apache License 2.0".
#
#     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, version 3 of the License.
#
#     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, see <http://www.gnu.org/licenses/>.
#
#     Please leave the whole of this copyright notice intact.
#

from __future__ import print_function

import os, sys, subprocess, difflib, re, tempfile

filename = sys.argv[1]
silent_mode = "silent" in sys.argv
ignore_stderr = "ignore_stderr" in sys.argv
exec_in_tmp = "exec_in_tmp" in sys.argv
expect_success = "expect_success" in sys.argv
expect_failure = "expect_failure" in sys.argv
python_debug = "python_debug" in sys.argv
two_step_execution = "two_step_execution" in sys.argv or os.name == "nt"
trace_command = "trace_command" in sys.argv

if "PYTHON" not in os.environ:
    os.environ[ "PYTHON" ] = "python"

if python_debug and os.path.exists( "/usr/bin/" + os.environ[ "PYTHON" ] + "-dbg" ):
    os.environ[ "PYTHON" ] += "-dbg"


print("Comparing output of '%s' using '%s' ..." % ( filename, os.environ[ "PYTHON" ] ))

if not silent_mode:
    print( "*******************************************************" )
    print( "CPython:" )
    print( "*******************************************************" )

if exec_in_tmp:
    filename = os.path.abspath( filename )

cpython_cmd = "%s -W ignore %s" % (
    os.environ[ "PYTHON" ],
    filename
)

nuitka_call = os.environ.get(
    "NUITKA",
    "%s %s" % (
        os.environ[ "PYTHON" ],
        os.path.abspath( os.path.join( os.path.dirname( __file__ ), "nuitka" ) )
    )
)

extra_options = os.environ.get( "NUITKA_EXTRA_OPTIONS", "" )

if python_debug:
    extra_options += " --python-debug"

if not two_step_execution:
    nuitka_cmd = "%s %s --exe --execute %s" % (
        nuitka_call,
        extra_options,
        filename
    )

    if trace_command:
        print( "Nuitka command:", nuitka_cmd )
else:
    nuitka_cmd1 = "%s %s --exe %s" % (
        nuitka_call,
        extra_options,
        filename
    )

    dir_match = re.search( r"--output-dir=(.*?)(\s|$)", extra_options )

    exe_filename = os.path.basename( filename )

    if filename.endswith( ".py" ):
        exe_filename = exe_filename[:-3] + ".exe"

    if dir_match:
        nuitka_cmd2 = os.path.join( dir_match.group( 1 ), exe_filename )
    else:
        nuitka_cmd2 = os.path.join( ".", exe_filename )

    if trace_command:
        print( "Nuitka command 1:", nuitka_cmd1 )
        print( "Nuitka command 2:", nuitka_cmd2 )

if exec_in_tmp:
    os.chdir( tempfile.gettempdir() )

process = subprocess.Popen(
    args   = cpython_cmd,
    stdout = subprocess.PIPE,
    stderr = subprocess.PIPE,
    shell  = True
)

stdout_cpython, stderr_cpython = process.communicate()
exit_cpython = process.returncode

if not silent_mode:
    print( stdout_cpython, end=' ' )

    if stderr_cpython:
        print( stderr_cpython )

if not silent_mode:
    print( "*******************************************************" )
    print( "Nuitka:" )
    print( "*******************************************************" )


if not two_step_execution:
    process = subprocess.Popen(
        args   = nuitka_cmd,
        stdout = subprocess.PIPE,
        stderr = subprocess.PIPE,
        shell  = True
    )

    stdout_nuitka, stderr_nuitka = process.communicate()
    exit_nuitka = process.returncode
else:
    process = subprocess.Popen(
        args   = nuitka_cmd1,
        stdout = subprocess.PIPE,
        stderr = subprocess.PIPE,
        shell  = True
    )

    stdout_nuitka1, stderr_nuitka1 = process.communicate()
    exit_nuitka1 = process.returncode

    if exit_nuitka1 != 0:
        exit_nuitka = exit_nuitka1
        stdout_nuitka, stderr_nuitka = stdout_nuitka1, stderr_nuitka1
    else:
        process = subprocess.Popen(
            args   = nuitka_cmd2,
            stdout = subprocess.PIPE,
            stderr = subprocess.PIPE,
            shell  = True
        )

        stdout_nuitka2, stderr_nuitka2 = process.communicate()
        stdout_nuitka, stderr_nuitka = stdout_nuitka1 + stdout_nuitka2, stderr_nuitka1 + stderr_nuitka2
        exit_nuitka = process.returncode

if not silent_mode:
    print( stdout_nuitka, end=' ' )

    if stderr_nuitka:
        print( stderr_nuitka )



ran_re = re.compile( r"^(Ran \d+ tests? in )\d+\.\d+s$" )
instance_re = re.compile( r"at (?:0x)?[0-9a-fA-F]+" )
compiled_function_re = re.compile( r"\<compiled function" )
compiled_genexpr_re = re.compile( r"\<compiled generator object \<(.*?)\>" )
compiled_generator_re = re.compile( r"\<compiled generator object (.*?) at" )
unbound_method_re = re.compile( r"bound compiled_method " )
compiled_type_re = re.compile( r"type 'compiled_" )
global_name_error_re = re.compile( r"global (name ')(.*?)(' is not defined)" )
module_repr_re = re.compile( r"(\<module '.*?' from ').*?('\>)" )

fromdate = None
todate = None

def makeDiffable( output ):
    result = []

    for line in output.split( b"\n" ):
        line = str( line )

        if line.endswith( "\r" ):
            line = line[:-1]

        line = instance_re.sub( r"at 0xxxxxxxxx", line )
        line = compiled_function_re.sub( r"<function", line )
        line = compiled_genexpr_re.sub( r"<generator object <\1>", line )
        line = compiled_generator_re.sub( r"<generator object \1 at", line )
        line = unbound_method_re.sub( r"bound method ", line )
        line = compiled_type_re.sub( r"type '", line )
        line = global_name_error_re.sub( r"\1\2\3", line )
        line = module_repr_re.sub( r"\1xxxxx\2", line )

        # Windows has a different os.path, update according to it.
        line = line.replace( "ntpath", "posixpath" )

        line = line.replace(
            "must be a mapping, not compiled_function",
            "must be a mapping, not function"
        )
        line = line.replace(
            "must be a sequence, not compiled_function",
            "must be a sequence, not function"
        )

        line = ran_re.sub( r"\1x.xxxs", line )

        if line.startswith( "Nuitka:WARNING:Cannot recurse to import" ):
            continue

        # TODO: The C++ compiler may give this warning when modules become too big.
        if line.endswith( "note: variable tracking size limit exceeded with -fvar-tracking-assignments, retrying without" ):
            del result[-1]
            continue

        # TODO: This is a bug potentially, occurs only for CPython when re-directed.
        if line == "Exception RuntimeError: 'maximum recursion depth exceeded while calling a Python object' in <type 'exceptions.AttributeError'> ignored":
            continue

        result.append( line )

    return result


def compareOutput( kind, out_cpython, out_nuitka ):
    diff = difflib.unified_diff(
        makeDiffable( out_cpython ),
        makeDiffable( out_nuitka ),
        "%s (%s)" % ( os.environ[ "PYTHON" ], kind ),
        "%s (%s)" % ( "nuitka", kind ),
        fromdate,
        todate,
        n=3
    )

    result = list( diff )

    if result:
        for line in result:
            print( line, end = "\n" if not line.startswith( "---" ) else "" )

        return 1
    else:
        return 0

exit_code_stdout = compareOutput( "stdout", stdout_cpython, stdout_nuitka )

if ignore_stderr:
    exit_code_stderr = 0
else:
    exit_code_stderr = compareOutput( "stderr", stderr_cpython, stderr_nuitka )

exit_code_return = exit_cpython != exit_nuitka

if exit_code_return:
    print( "Exit codes %d (CPython) != %d (Nuitka)" % ( exit_cpython, exit_nuitka ) )

exit_code = exit_code_stdout or exit_code_stderr or exit_code_return

if exit_code:
    sys.exit( "Error, outputs differed." )

if not silent_mode:
    print( "OK, same outputs." )

if expect_success and exit_cpython != 0:
    sys.exit( "Unexpected error exit from CPython." )

if expect_failure and exit_cpython == 0:
    sys.exit( "Unexpected success exit from CPython." )
