# Copyright (C) 2009-2011, Travis Bear
# All rights reserved.
#
#
# This file is part of Grinder Analyzer.
#
# With contributions from:
#    anonymous
#
# Grinder Analyzer 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; either version 2 of the License, or
# (at your option) any later version.
#
# Grinder Analyzer 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 Grinder Analyzer; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA

# Add .jar files to the classpath
import classpathtool

# python imports
import os.path
import sys
import os
import fileutils
import getopt

# Java imports
import org.htmlparser.Parser
import org.htmlparser.filters.TagNameFilter


# Jython / analyzer imports
import analyzer

"""
This tool was written by Travis Bear.

Tool to compare relative performance of different grinder runs.  Can compare
the summary data contained in the grinder out* files, or the summaries in
.html reports generated by grinder analyzer.

Classes in this module:

   TxData:          Holds data on a single transaction -- response time, length,
                    etc.  One instance of this class is created for each unique
                    transaction in your Grinder test.

   TxDiffData:      Holds data on the changes between two instances of TxData

   Configuration:   Config data for the app.  Populated based on command-line
                    args.

Most of the work in this initial implementation is done inside module-level
functions.  In the future it is likely this will be refactored into an entirely
object-oriented approach.

Output to .html format is planned but did not make it into this release.
                    
"""


class TxData:
    """
    Holds data to be diffed for a single transaction
    """
    response_time = None
    response_length = None
    pass_rate = None

    def __init__(self, rtime, rlen):
        self.response_time = float(rtime)
        self.response_length = float(rlen)

    def __string__(self):
        return "response time: " + str(self.response_time) \
            + ", response length: " + str(self.response_length)


class TxDiffData:
    delta_rtime = 0.0
    delta_rtime_percent = 0.0
    delta_rlen = 0.0
    delta_rlen_percent = 0.0
    delta_passrate = 0.0
    txdata1 = None
    txdata2 = None

    def __init__(self, tx1, tx2):
        self.txdata1 = tx1
        self.txdata2 = tx2
        self.delta_rtime = tx2.response_time - tx1.response_time
        self.delta_rtime_percent = self._getPercentDiff(tx1.response_time, tx2.response_time)
        self.delta_rlen = tx2.response_length - tx1.response_length
        self.delta_rlen_percent = self._getPercentDiff(tx1.response_length, tx2.response_length)

    def _getPercentDiff(self, val1, val2):
        if val1 == val2:
            return 0
        if val1 == 0:
            # prevent divide by zero
            return 0
        return 100.0 * (val2 - val1) / val1

    def __string__(self):
        return "rt1: " + str(self.txdata1.response_time) \
            + ", rt2: " + str(self.txdata2.response_time) \
            + ", rt_abs: " + str(self.delta_rtime) \
            + ", rt_percent: " + str(self.delta_rtime_percent)


class Configuration:
    output = "text" # report can also be sent to .csv or .html
    results = "all" # can also be "union" -- show only rows in common
    columns = "all" # output specific columns only ("len", "time")
    different = -1.0 # threshold of difference % before row is displayed
    file1 = ""
    file2 = ""
    results_file = ""

    def __init__(self):
        """
        Determine configs based on command-line options/args
        """
        try:
            long_opts = ["help", \
                "output=", \
                "results=", \
                "columns=", \
                "different=", \
                "file="]
            opts, args = getopt.getopt(sys.argv[1:], "ho:r:c:d:f:", long_opts)
            #print "opts = " + str(opts)
            #print "args = %s, len(args)=%d" % (str(args), len(args))
            if len(args) != 2:
                usage()
            for o, a in opts:
                #print str(o) + "  - " + str(a)
                if o == "-v":
                    verbose = True
                elif o in ("-h", "--help"):
                    usage()
                elif o in ("-o", "--output"):
                    self.output = a
                elif o in ("-r", "--results"):
                    self.results = a
                elif o in ("-c", "--columns"):
                    if ["len", "time"].__contains__(a):
                        self.columns = a
                    else:
                        print "FATAL: Invalid column option '%s'" % a
                        usage()
                elif o in ("-d", "--different"):
                    self.different = float(a)
                elif o in ("-f", "--file"):
                    self.results_file = a
                else:
                    assert False, "unhandled option " + str(o) + ", " + str(a)
            self.file1 = args[0]
            self.file2 = args[1]
        except getopt.GetoptError, err:
            # print help information and exit:
            print "FATAL: " + str(err) # will print something like "option -a not recognized"
            usage()
        if self.different > 0:
            self.results = "union"

    def __string__(self):
        s = "output: " + self.output + "\n" \
            + "results: " + self.results + "\n" \
            + "columns: " + self.columns + "\n" \
            + "different: " + str(self.different) + "\n" \
            + "file1: " + self.file1 + "\n" \
            + "file2: " + self.file2
        return



def usage():
    message = """
    usage: jython difftool.py [options] file1 file2

    file1 and file2 may either be grinder out files or grinder analyzer .html
    reports.  Options include:

        -h, --help        display this usage message

        -o, --output      text (default) | csv

        -r, --results     all (default) | union (squelches non-shared rows)

        -c, --columns     all (default) | time(only) | len(only)

        -d, --different   only show rows that differ by this percentage.  
                          
        -f, --file        by default difftool sends text output to standard out.
                          With this option you may specify a file for results to
                          be written to.  (Html and csv output are only written
                          to files so this setting has no effect for those
                          options.)

    """
    print message
    sys.exit(1)


def build_html_tree(file_name):
    """
    Uses the excellent 'htmlparser' library to parse the .html document
    and extract the relevant transaction data:  transaction name, response
    time, and response length.
    """
    print "Building html tree from " + file_name

    tx_data_set = {}
    html = open(file_name).read()
    parser = org.htmlparser.Parser(html)
    tableFilter = org.htmlparser.filters.TagNameFilter("table")
    tableNodes = parser.extractAllNodesThatMatch(tableFilter)
    tableNode = tableNodes.elementAt(0)
    NAME_INDEX = 1
    RT_INDEX = 9
    RLEN_INDEX = 13
    TRANSACTION_NAME = "Transacton Name"
    for row in tableNode.getRows():
        nameNode = row.childAt(NAME_INDEX).getFirstChild()
        name = nameNode.toPlainTextString().strip()
        if name == TRANSACTION_NAME:
            continue
        responseTime = nameNode = row.childAt(RT_INDEX).toPlainTextString().strip()
        responseLen = nameNode = row.childAt(RLEN_INDEX).toPlainTextString().strip()
        tx_data_set[name] = TxData(responseTime, responseLen)
    return tx_data_set



def build_logfile_tree(file_name):
    """ Builds an object model from a grinder out_ file. """
    print "Building logfile tree from " + file_name
    tx_data_set = {}
    # TODO -- this smells bad
    reporter = analyzer.getReporter(file_name)
    finalOutLines = fileutils.reverseSeek (file_name, analyzer.TABLE_MARKER)
    for line in finalOutLines:
        if line.find("Test") == 0 or line.find("Test") == 1:
            testName = line.split('"')[1]
            words = line.split()
            responseTime = words[reporter.getMeanTimeColumn()]
            responseLength = words[reporter.getResponseLengthColumn()]
            tx_data_set[testName] = TxData(responseTime, responseLength)
        if line.startswith(TOTALS):
            # fix the TOTALS row, which has one fewer column
            line = line.replace(TOTALS, TOTALS + " 0")
            words = line.split()
            responseTime = words[reporter.getMeanTimeColumn()]
            responseLength = words[reporter.getResponseLengthColumn()]
            tx_data_set[TOTALS] = TxData(responseTime, responseLength)
    # catch corrupted or incomplete grinder out files
    if (tx_data_set.__len__() == 0):
        msg = """
            FATAL:  Incomplete or corrupted grinder out file.  No summary data containing
            test number/name mappings found in """ + file_name
        print msg
        sys.exit(1)
    return tx_data_set


def build_tree(file):
    if file.endswith(".log"):
        return build_logfile_tree(file)
    elif file.endswith(".html"):
        return build_html_tree(file)
    else:
        print "FATAL: unrecognized file type '" + file + "'"
        usage()


def getTextOutput(set1_only, set2_only, diff_set):

    DELIM = " | "
    TIME1 = "   time 1"
    TIME2 = "   time 2"
    TIME_DIFF = "time tiff"
    TIME_DIFF_P = "time diff %"
    RESPONSE_TIME = "response time"

    LENGTH1 = " length 1"
    LENGTH2 = " length 2"
    LENGTH_DIFF = "length diff"
    LENGTH_DIFF_P = "length diff %"
    RESPONSE_LENGTH = "response length"

    def getTxLen(set):
        maxLen = 0
        for txName in set.keys():
            if len(txName) > maxLen:
                maxLen = len(txName)
        return maxLen

    def getTxLine(name, tx, maxLen):
        line = DELIM + name.rjust(maxLen) + DELIM
        if config.columns == "all" or config.columns == "time":
            line += str(tx.response_time).rjust(len(RESPONSE_TIME)) + DELIM
        if config.columns == "all" or config.columns == "len":
            line += str(tx.response_length).rjust(len(RESPONSE_LENGTH)) + DELIM
        return line + os.linesep

    def getTxHeaders(maxLen):
        line = DELIM + "Tx name".rjust(maxLen) + DELIM
        if config.columns == "all" or config.columns == "time":
            line += RESPONSE_TIME + DELIM
        if config.columns == "all" or config.columns == "len":
            line += RESPONSE_LENGTH + DELIM
        dashes = " " + "".join(["-" for i in range(1, len(line)-1)]) + os.linesep
        return dashes + line + os.linesep + dashes

    def getTxFooter(maxLen):
        return getTxHeaders(maxLen).split(os.linesep)[0] + os.linesep

    def getDiffHeaders(maxLen):
        line = DELIM + "Transaction".rjust(maxLen) + DELIM
        if config.columns == "all" or config.columns == "time":
            line += TIME1 + DELIM
            line += TIME2 + DELIM
            line += TIME_DIFF + DELIM
            line += TIME_DIFF_P + DELIM
        if config.columns == "all" or config.columns == "len":
            line += LENGTH1 + DELIM
            line += LENGTH2 + DELIM
            line += LENGTH_DIFF + DELIM
            line += LENGTH_DIFF_P + DELIM
        dashes = " " + "".join(["-" for i in range(1, len(line)-1)]) + os.linesep
        return dashes + line + os.linesep + dashes

    def getDiffLine(name, diffItem, maxLen):
        line = DELIM + name.rjust(maxLen) + DELIM
        if config.columns == "all" or config.columns == "time":
            intSpace = len(TIME1)
            command = 'line += "%' + str(intSpace) + '.1f%s" %(diffItem.txdata1.response_time, DELIM)'
            exec command in locals()
            intSpace = len(TIME2)
            command = 'line += "%' + str(intSpace) + '.1f%s" %(diffItem.txdata2.response_time, DELIM)'
            exec command in locals()
            intSpace = len(TIME_DIFF)
            command = 'line += "%' + str(intSpace) + '.1f%s" %(diffItem.delta_rtime, DELIM)'
            exec command in locals()
            intSpace = len(TIME_DIFF_P)
            command = 'line += "%' + str(intSpace) + '.2f%s" %(diffItem.delta_rtime_percent, DELIM)'
            exec command in locals()
        if config.columns == "all" or config.columns == "len":
            intSpace = len(LENGTH1)
            command = 'line += "%' + str(intSpace) + '.1f%s" %(diffItem.txdata1.response_length, DELIM)'
            exec command in locals()
            intSpace = len(LENGTH2)
            command = 'line += "%' + str(intSpace) + '.1f%s" %(diffItem.txdata2.response_length, DELIM)'
            exec command in locals()
            intSpace = len(LENGTH_DIFF)
            command = 'line += "%' + str(intSpace) + '.1f%s" %(diffItem.delta_rlen, DELIM)'
            exec command in locals()
            intSpace = len(LENGTH_DIFF_P)
            command = 'line += "%' + str(intSpace) + '.2f%s" %(diffItem.delta_rlen_percent, DELIM)'
            exec command in locals()
        return line + os.linesep

    def getDiffFooter(maxLen):
        return getDiffHeaders(maxLen).split(os.linesep)[0] + os.linesep

    maxLen = getTxLen(set1_only)
    output = os.linesep
    if config.results == "all":
        output += "SET ONE ONLY" + os.linesep
        output += getTxHeaders(maxLen)
        for key in set1_only.keys():
            output += getTxLine(key, set1_only[key], maxLen)
        output += getTxFooter(maxLen)

    output += os.linesep + "DIFFERENCES" + os.linesep
    maxLen = getTxLen(diff_set)
    output += getDiffHeaders(maxLen)
    keys = diff_set.keys()
    keys.sort()
    # ensure the "Totals" row goes at the end of the list
    if keys.__contains__(TOTALS):
        keys.remove(TOTALS)
        keys.append(TOTALS)
    for key in keys:
        output += getDiffLine(key, diff_set[key], maxLen)
    output += getDiffFooter(maxLen)

    output += os.linesep
    if config.results == "all":
        output += "SET TWO ONLY" + os.linesep
        maxLen = getTxLen(set2_only)
        if config.results == "all":
            output += getTxHeaders(maxLen)
            for key in set2_only.keys():
                output += getTxLine(key, set2_only[key], maxLen)
        output += getTxFooter(maxLen)
    return output



def doHtmlOutput(set1_only, set2_only, diff_set):
    print "Can't do html output yet."
    sys.exit(1)


def generateCsvOutput(set1_only, set2_only, diff_set):
    
    DELIM = ","
    TIME1 = "time_1"
    TIME2 = "time_2"
    TIME_DIFF = "time_tiff"
    TIME_DIFF_P = "time_diff_%"
    RESPONSE_TIME = "response_time"

    LENGTH1 = "length_1"
    LENGTH2 = "length_2"
    LENGTH_DIFF = "length_diff"
    LENGTH_DIFF_P = "length_diff_%"
    RESPONSE_LENGTH = "response_length"
    
    def getCsvUnionOutput(diff_set):
        def getHeaderRow():
            line = "Tx name" + DELIM
            if config.columns == "all" or config.columns == "time":
                line += TIME1 + DELIM
                line += TIME2 + DELIM
                line += TIME_DIFF + DELIM
                line += TIME_DIFF_P + DELIM
            if config.columns == "all" or config.columns == "len":
                line += LENGTH1 + DELIM
                line += LENGTH2 + DELIM
                line += LENGTH_DIFF + DELIM
                line += LENGTH_DIFF_P + DELIM
            return line[:len(line)-1] + os.linesep

            
        def getBodyRow(diffItem, name):
            line = name + DELIM
            if config.columns == "all" or config.columns == "time":
                line += "%.1f%s" % (diffItem.txdata1.response_time, DELIM)
                line += "%.1f%s" % (diffItem.txdata2.response_time, DELIM)
                line += "%.1f%s" % (diffItem.delta_rtime, DELIM)
                line += "%.1f%s" % (diffItem.delta_rtime_percent, DELIM)
            if config.columns == "all" or config.columns == "len":
                line += "%.1f%s" % (diffItem.txdata1.response_length, DELIM)
                line += "%f%s" % (diffItem.txdata2.response_length, DELIM)
                line += "%.1f%s" % (diffItem.delta_rlen, DELIM)
                line += "%f%s" % (diffItem.delta_rlen_percent, DELIM)
            return line[:len(line)-1] + os.linesep


        output = getHeaderRow()
        
        keys = diff_set.keys()
        keys.sort()
        # ensure the "Totals" row goes at the end of the list
        if keys.__contains__(TOTALS):
            keys.remove(TOTALS)
            keys.append(TOTALS)    
        for name in keys:
            output += getBodyRow(diff_set[name], name)
        return output

    def getCsvNonUnionOutput(set):
        def getHeaderRow():
            line = "Transaction Name,"
            if config.columns == "all" or config.columns == "time":
                line += RESPONSE_TIME + DELIM
            if config.columns == "all" or config.columns == "len":
                line += RESPONSE_LENGTH + DELIM
            return line[:len(line)-1] + os.linesep

        def getBodyRow(diffItem, name):
            line = name + DELIM
            if config.columns == "all" or config.columns == "time":
                line += str(diffItem.response_time) + DELIM
            if config.columns == "all" or config.columns == "len":
                line += str(diffItem.response_length) + DELIM
            return line[:len(line)-1] + os.linesep

        output = getHeaderRow()
        for name in set.keys():
            output += getBodyRow(set[name], name)
        return output

    if config.results == "all":
        out(getCsvNonUnionOutput(set1_only), "set1.csv")
        print "Generated set1.csv"
    out(getCsvUnionOutput(diff_set), "diff.csv")
    print "Generated diff.csv"
    if config.results == "all":
        out(getCsvNonUnionOutput(set2_only), "set2.csv")
        print "Generated set2.csv"

# generate the config
config = Configuration()
TOTALS = "Totals"

def out(text, fileName=config.results_file):
    if fileName is not "":
        file = open(fileName, 'w')
        file.write(text)
        file.close()
        print "Results written to %s" % fileName
    else:
        print text

def main():
    if not os.path.exists(config.file1):
        print "FATAL: '" + config.file1 + "' not found."
        sys.exit(1)
    if not os.path.exists(config.file2):
        print "FATAL: '" + config.file2 + "' not found."
        sys.exit(1)
    set1 = build_tree(config.file1)
    set2 = build_tree(config.file2)
    set1_only_keys = [item for item in set1.keys() if not set2.has_key(item)]
    set2_only_keys = [item for item in set2.keys() if not set1.has_key(item)]
    intersecting_keys = [item for item in set1.keys() if set2.has_key(item)]
    diff_set = {} #key: tx name,  value: txDiffData
    set1_only = {}  #key: tx name,  value: txData
    set2_only = {}  #key: tx name,  value: txData
    for key in set1_only_keys:
        set1_only[key] = set1[key]
    for key in set2_only_keys:
        set2_only[key] = set2[key]
    for key in intersecting_keys:
        diffItem = TxDiffData(set1[key], set2[key])
        if config.columns == "all":
            if diffItem.delta_rtime_percent > config.different or diffItem.delta_rlen_percent > config.different:
                diff_set[key] = diffItem
        if config.columns == "time":
            if diffItem.delta_rtime_percent > config.different:
                diff_set[key] = diffItem
        if config.columns == "len":
            if diffItem.delta_rlen_percent > config.different:
                diff_set[key] = diffItem
    #print "Output: '%s'" % config.output
    if config.output == "text":
        out(getTextOutput(set1_only, set2_only, diff_set))
    if config.output == "csv":
        generateCsvOutput(set1_only, set2_only, diff_set)
    if config.output == "html":
        generateHtmlOutput(set1_only, set2_only, diff_set)

if __name__ == "__main__":
    main()
