#!/usr/bin/env python
#     Copyright 2013, Kay Hayen, mailto:kay.hayen@gmail.com
#
#     Part of "Nuitka", an optimizing Python compiler that is compatible and
#     integrates with CPython, but also works on its own.
#
#     Licensed under the Apache License, Version 2.0 (the "License");
#     you may not use this file except in compliance with the License.
#     You may obtain a copy of the License at
#
#        http://www.apache.org/licenses/LICENSE-2.0
#
#     Unless required by applicable law or agreed to in writing, software
#     distributed under the License is distributed on an "AS IS" BASIS,
#     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#     See the License for the specific language governing permissions and
#     limitations under the License.
#

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
ignore_warnings = "ignore_warnings" 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
remove_output = "remove_output" in sys.argv

if "PYTHONHASHSEED" not in os.environ:
    os.environ[ "PYTHONHASHSEED" ] = "0"

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

if python_debug and os.path.exists( os.path.join( "/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 remove_output:
    extra_options += " --remove-output"

exe_filename = os.path.basename( filename )

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

exe_filename += ".exe"


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 )

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

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

if exec_in_tmp:
    tmp_dir = tempfile.gettempdir()

    # Try to avoid RAM disk /tmp and use the disk one instead.
    if tmp_dir == "/tmp" and os.path.exists( "/var/tmp" ):
        tmp_dir = "/var/tmp"

    os.chdir( tmp_dir )

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

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

def displayCPython():
    print( stdout_cpython, end=' ' )

    if stderr_cpython:
        print( stderr_cpython )

if not silent_mode:
    displayCPython()

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:
        path_sep = ";" if os.name == "nt" else ":"
        os.environ[ "PYTHONPATH" ] = path_sep.join( [ os.path.dirname( filename ) ] + os.environ.get( "PYTHONPATH", "" ).split( path_sep ) )

        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_frame_re = re.compile( r"\<compiled_frame" )
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_" )
compiled_generator_object_re = re.compile( r"'compiled_generator' object" )
global_name_error_re = re.compile( r"global (name ')(.*?)(' is not defined)" )
module_repr_re = re.compile( r"(\<module '.*?' from ').*?('\>)" )
non_ascii_error_rt = re.compile( r"(SyntaxError: Non-ASCII character.*? on line) \d+" )
python_win_lib_re = re.compile( r"[a-zA-Z]:\\\\?[Pp]ython(\d\d\\\\?)[Ll]ib" )

def makeDiffable( output ):
    result = []

    # fix import readline cause output sometimes startswith \x1b[?1034h
    m = re.match( b'\\x1b\\[[^h]+h', output )
    if m:
        output = output[len( m.group() ):]

    for line in output.split( b"\n" ):
        if type( line ) is not str:
            line = line.decode( "utf-8" )

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

        if line.startswith( "[" ) and line.endswith( "refs]" ):
            continue

        if ignore_warnings and line.startswith( "Nuitka:WARNING" ):
            continue

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

        line = instance_re.sub( r"at 0xxxxxxxxx", line )
        line = compiled_function_re.sub( r"<function", line )
        line = compiled_frame_re.sub( r"<frame", 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 = compiled_generator_object_re.sub( r"'generator' object", line )
        line = global_name_error_re.sub( r"\1\2\3", line )
        line = module_repr_re.sub( r"\1xxxxx\2", line )
        line = line.replace( "'compiled_module'", "'module'" )
        line = line.replace( "'compiled_function'", "'function'" )
        line = non_ascii_error_rt.sub( r"\1 xxxx", 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 )

        # This is a bug potentially, occurs only for CPython when re-directed, we are
        # going to ignore the issue as Nuitka is fine.
        if line == "Exception RuntimeError: 'maximum recursion depth exceeded while calling a Python object' in <type 'exceptions.AttributeError'> ignored":
            continue

        # This is also a bug potentially, but only visible under
        # CPython
        line = python_win_lib_re.sub( r"C:\Python\1Lib", line )

        # This is a bug with clang potentially, can't find out why it says that.
        if line == "/usr/bin/ld: warning: .init_array section has zero size":
            continue

        # This is for NetBSD, which seems to build libpython so it gives such warnings.
        if "() possibly used unsafely, use mkstemp() or mkdtemp()" in line:
            continue

        result.append( line )

    return result


def compareOutput( kind, out_cpython, out_nuitka ):
    fromdate = None
    todate = None

    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 expect_success and exit_cpython != 0:
    if silent_mode:
        displayCPython()

    sys.exit( "Unexpected error exit from CPython." )

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

if remove_output and os.path.exists( nuitka_cmd2 ):
    os.unlink( nuitka_cmd2 )

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