#!/usr/bin/python
##
# CU - C unit testing framework
# ---------------------------------
# Copyright (c)2007,2008 Daniel Fiser <danfis@danfis.cz>
#
#
#  This file is part of CU.
#
#  CU is free software; you can redistribute it and/or modify
#  it under the terms of the GNU Lesser General Public License as
#  published by the Free Software Foundation; either version 3 of
#  the License, or (at your option) any later version.
#
#  CU 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 Lesser General Public License for more details.
#
#  You should have received a copy of the GNU Lesser General Public License
#  along with this program.  If not, see <http://www.gnu.org/licenses/>.
#

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

EPS = 0.6
BASE_DIR = "."
MAX_DIFF_LINES = 20
EXACT = False

PROGRESS_ON = True
MSG_BASE = ""

class Hunk:
    """ This class represents one hunk from diff. """

    def __init__(self):
        self.added = []
        self.deleted = []
        self.lines = []

        # to identify lines with floating point numbers
        self.re_is_num = re.compile("^.*[0-9].*$")

        # pattern to match floating point number
        self.num_pattern = r"-?(?:(?:[0-9]+(?:\.[0-9]*)?)|(?:\.[0-9]+))(?:[eE]-?[0-9]+)?"
        self.re_num = re.compile(self.num_pattern)

    def numLines(self):
        return len(self.lines)
    def numLinesAdded(self):
        return len(self.added)
    def numLinesDeleted(self):
        return len(self.deleted)

    def addLineAdded(self, line):
        self.added.append(line)
    def addLineDeleted(self, line):
        self.deleted.append(line)
    def addLine(self, line):
        self.lines.append(line)

    def getLines(self):
        return self.lines
    def getLinesAdded(self):
        return self.added
    def getLinesDeleted(self):
        return self.deleted

    def __eq(self, num1, num2):
        """ Returns True if num1 equals to num2 with respect to EPS
            (defined above) """
        return math.fabs(num1 - num2) < EPS

    def checkFloats(self):
        """ This method try to check if only difference between added and
            deleted lines of this hunk is different precission of floating
            point numbers
        """

        # If number of added and deleted lines differs, then there is more
        # differences that precission of floating point numbers
        if self.numLinesAdded() != self.numLinesDeleted():
            return False

        for i in xrange(0, self.numLinesAdded()):
            # if any line does not contain number - return False because
            # there must be more differences than in numbers
            if not self.re_is_num.match(self.added[i]) \
               or not self.re_is_num.match(self.deleted[i]):
               return False

            line1 = self.added[i]
            line2 = self.deleted[i]

            # Extract all floating point numbers from each line
            nums1 = self.re_num.findall(line1)
            nums2 = self.re_num.findall(line2)
            # and remove all empty strings
            nums1 = filter(lambda x: len(x) > 0, nums1)
            nums2 = filter(lambda x: len(x) > 0, nums2)

            # if length of list nums1 does not equal to length of nums2
            # return False
            if len(nums1) != len(nums2):
                return False

            # iterate trough all numbers
            for j in xrange(0, len(nums1)):
                # if numbers do not equal to each other return False
                if not self.__eq(float(nums1[j]), float(nums2[j])):
                    return False

            # compare the rest of lines
            line1 = self.re_num.sub("", line1)
            line2 = self.re_num.sub("", line2)
            if line1 != line2:
                return False

        # If it does not fail anywhere, added and deleted lines must be
        # same
        return True


class Diff:
    """ Represents whole diff. """

    def __init__(self):
        self.hunks = []
        self.lines = 0
        self.omitted_lines = 0

    def addHunk(self, hunk):
        self.hunks.append(hunk)
        self.lines += hunk.numLines()

    def numLines(self):
        return self.lines
    def numOmittedLines(self):
        return self.omitted_lines

    def getHunks(self):
        return self.hunks
    def numHunks(self):
        return len(self.hunks)

    def checkFloats(self):
        """ Will call method checkFloats on each hunk """
        hks = self.hunks[:]
        self.hunks = []
        self.lines = 0
        for h in hks:
            if not h.checkFloats():
                self.hunks.append(h)
                self.lines += h.numLines()
            else:
                self.omitted_lines += h.numLines()
            


class Parser:
    def __init__(self, fin):
        self.fin = fin
        self.line = ""
        self.diff = Diff()
        self.cur_hunk = None

        # to recognize beginning of hunk:
        self.re_hunk = re.compile(r"^[0-9]*(,[0-9]*){0,1}[a-zA-Z]?[0-9]*(,[0-9]*){0,1}$")

        self.re_added = re.compile(r"^> (.*)$")
        self.re_deleted = re.compile(r"^< (.*)$")

    def __readNextLine(self):
        self.line = self.fin.readline()
        if len(self.line) == 0:
            return False
        return True

    def parse(self):
        global PROGRESS_ON
        global MSG_BASE

        num_lines = 0
        while self.__readNextLine():
            # beggining of hunk
            if self.re_hunk.match(self.line):
                if self.cur_hunk is not None:
                    self.diff.addHunk(self.cur_hunk)
                self.cur_hunk = Hunk()
                self.cur_hunk.addLine(self.line)

            # line added
            match = self.re_added.match(self.line)
            if match is not None:
                self.cur_hunk.addLine(self.line)
                self.cur_hunk.addLineAdded(match.group(1))

            # line deleted
            match = self.re_deleted.match(self.line)
            if match is not None:
                self.cur_hunk.addLine(self.line)
                self.cur_hunk.addLineDeleted(match.group(1))

            num_lines += 1

            if PROGRESS_ON and num_lines % 50 == 0:
                print MSG_BASE, "[ %08d ]" % num_lines, "\r",
                sys.stdout.flush()

        # last push to list of hunks
        if self.cur_hunk is not None:
            self.diff.addHunk(self.cur_hunk)

        if PROGRESS_ON:
            print MSG_BASE, "           ", "\r",
            sys.stdout.flush()
        
    def getDiff(self):
        return self.diff


def regressionFilesInDir():
    """ Returns sorted list of pairs of filenames where first name in pair
        is tmp. file and second corresponding file with saved regressions.
    """

    re_tmp_out_file = re.compile(r"tmp\.(.*\.out)")
    re_tmp_err_file = re.compile(r"tmp\.(.*\.err)")
    files = []

    all_files = os.listdir(".")
    all_files.sort()
    for file in all_files:
        res = re_tmp_out_file.match(file)
        if res is not None:
            fname = res.group(1)
            tmp = [file, ""]
            for file2 in all_files:
                if file2 == fname:
                    tmp = [file, file2,]
                    break
            files.append(tmp)

        res = re_tmp_err_file.match(file)
        if res is not None:
            fname = res.group(1)
            tmp = [file, ""]
            for file2 in all_files:
                if file2 == fname:
                    tmp = [file, file2,]
                    break
            files.append(tmp)

    return files


def MSG(str = "", wait = False):
    if wait:
        print str,
    else:
        print str
def MSGOK(prestr = "", str = "", poststr = ""):
    print prestr, "\033[0;32m" + str + "\033[0;0m", poststr
def MSGFAIL(prestr = "", str = "", poststr = ""):
    print prestr, "\033[0;31m" + str + "\033[0;0m", poststr
def MSGINFO(prestr = "", str = "", poststr = ""):
    print prestr, "\033[0;33m" + str + "\033[0;0m", poststr
def dumpLines(lines, prefix = "", wait = False, max_lines = -1):
    line_num = 0
    if wait:
        for line in lines:
            print prefix, line,
            line_num += 1
            if max_lines >= 0 and line_num > max_lines:
                break
    else:
        for line in lines:
            print prefix, line
            line_num += 1
            if max_lines >= 0 and line_num > max_lines:
                break

def main(files):
    global MSG_BASE

    # As first compute length of columns
    len1 = 0
    len2 = 0
    for filenames in files:
        if len(filenames[0]) > len1:
            len1 = len(filenames[0])
        if len(filenames[1]) > len2:
            len2 = len(filenames[1])

    for filenames in files:
        if len(filenames[1]) == 0:
            MSGFAIL("", "===", "Can't compare %s %s, bacause %s does not exist!" % \
                    (filenames[0], filenames[0][4:], filenames[0][4:]))
            continue

        cmd = ["diff", filenames[0], filenames[1]]
        MSG_BASE = "Comparing %s and %s" % \
                (filenames[0].ljust(len1) ,filenames[1].ljust(len2))
        if not PROGRESS_ON:
            print MSG_BASE, 
            sys.stdout.flush()

        pipe = Popen(cmd, stdout=PIPE)
        parser = Parser(pipe.stdout)
        parser.parse()
        diff = parser.getDiff()
        if not EXACT:
            diff.checkFloats()

        if PROGRESS_ON:
            print MSG_BASE,

        if diff.numHunks() == 0:
            MSGOK(" [", "OK", "]")
            if diff.numOmittedLines() > 0:
                MSGINFO(" -->", str(diff.numOmittedLines()) + " lines from diff omitted")
        else:
            MSGFAIL(" [", "FAILED", "]")
            if diff.numOmittedLines() > 0:
                MSGINFO(" -->", str(diff.numOmittedLines()) + " lines from diff omitted")
            MSGINFO(" -->", "Diff has " + str(diff.numLines()) + " lines")

            if diff.numLines() <= MAX_DIFF_LINES:
                MSGINFO(" -->", "Diff:")
                for h in diff.getHunks():
                    dumpLines(h.getLines(), "   |", True)
            else:
                MSGINFO(" -->", "Printing only first " + str(MAX_DIFF_LINES) + " lines:")
                lines = []
                for h in diff.getHunks():
                    lines += h.getLines()
                    if len(lines) > MAX_DIFF_LINES:
                        break;
                dumpLines(lines, "   |", True, MAX_DIFF_LINES)
                    
def usage():
    print "Usage: " + sys.argv[0] + " [ OPTIONS ] [ directory, [ directory, [ ... ] ] ]"
    print ""
    print " OPTIONS:"
    print "     --help / -h       none   Print this help"
    print "     --exact / -e      none   Switch do exact comparasion of files"
    print "     --not-exact / -n  none   Switch do non exact comparasion of files (default behaviour)"
    print "     --max-diff-lines  int    Maximum of lines of diff which can be printed (default " + str(MAX_DIFF_LINES) + ")"
    print "     --eps             float  Precision of floating point numbers (epsilon) (default " + str(EPS) + ")"
    print "     --no-progress     none   Turn off progress bar"
    print "     --progress        none   Turn on progress bar (default)"
    print ""
    print " This program is able to compare files with regressions generated by CU testsuites."
    print " You can specify directories which are to be searched for regression files."
    print " In non exact copmarasion mode (which is default), this program tries to compare"
    print " floating point numbers in files with respect to specified precision (see --eps) and"
    print " those lines which differ only in precission of floating point numbers are omitted."
    print ""
    sys.exit(-1)



# Init:

# Set up base dir
BASE_DIR = os.getcwd()

# Parse command line options:
optlist, args = gnu_getopt(sys.argv[1:],
                           "hen",
                           ["help", "max-diff-lines=", "eps=", \
                            "exact", "not-exact", \
                            "no-progress", "progress"])
for opt in optlist:
    if opt[0] == "--help" or opt[0] == "-h":
        usage()
    if opt[0] == "--exact" or opt[0] == "-e":
        EXACT = True
    if opt[0] == "--not-exact" or opt[0] == "-n":
        EXACT = False
    if opt[0] == "--max-diff-lines":
        MAX_DIFF_LINES = int(opt[1])
    if opt[0] == "--eps":
        EPS = float(opt[1])
    if opt[0] == "--no-progress":
        PROGRESS_ON = False
    if opt[0] == "--progress":
        PROGRESS_ON = True

if len(args) == 0:
    files = regressionFilesInDir()
    main(files)
else:
    for dir in args:
        os.chdir(BASE_DIR)

        MSGINFO()
        MSGINFO("", "Processing directory '" + dir + "':")
        MSGINFO()
        try:
            os.chdir(dir)
        except:
            MSGFAIL("  -->", "Directory '" + dir + "' does not exist.")
        files = regressionFilesInDir()
        main(files)

sys.exit(0)
