#!/usr/bin/python
#
# kcov-merge - merge line-based hit information from cobertura.xml files generated by
#              kcov and generate aggregate coverage information over all files as well
#              as coverage information per file
#
# Usage:
#
# kcov-merge [-o <outputDirectory> ] dir1 dir2 .. dirn
#

import os
import shutil
import sys
import time
from optparse import OptionParser
import xml.dom.minidom


from xml.dom.minidom import Node

def parseFile(name):
    try:
        dom = xml.dom.minidom.parse(name)
    except:
        return None

    return dom


#-----------------------------------------------
class CovFile:

    def __init__(self, name, filename, low, high):
        self.name = name
        self.filename = filename
        self.low = low
        self.high = high

        # Dict of "line id" -> hits
        self.covLines={}
        # Coverage stats
        self.rate=0.0
        self.lines=0
        self.hits=0
        

    # classElt is the <class> element from cobertura XML
    def parse(self,classElt):
        lines = classElt.getElementsByTagName('lines')
        for linesElt in lines:
            for line in linesElt.childNodes:
                if line.nodeType == Node.ELEMENT_NODE:
                    num=line.attributes['number'].value
                    lineNum=int(num)
                    hits=line.attributes['hits'].value

                    if lineNum in self.covLines:
                        self.covLines[lineNum] = self.covLines[lineNum] + int(hits)
                    else:
                        self.covLines[lineNum] = int(hits)

    def calculateCoverage(self):
        self.hits = 0
        self.lines = 0
        for line in self.covLines:
            self.lines=self.lines+1
            if self.covLines[line] > 0:
                self.hits=self.hits+1
        self.rate = (float(self.hits) / float(self.lines)) * 100

    def displayCoverage(self,indexFile,coberturaFile):
        # print ('    File %s covered lines %d total lines %d coverage %0.1f%%' % (self.filename, self.hits, self.lines, self.rate))

        # Specify the type of coverage for the line based on the specified low and high limits
        type='lineNoCov'
        if self.rate > self.low:
            type='linePartCov'
        if self.rate > self.high:
            type='lineCov'
        indexFile.write("{'title':'%s','summary_name':'%s','covered_class':'%s','covered':'%0.1f','covered_lines':'%d','uncovered_lines':'%d','total_lines':'%d' },\n"
                         % (self.filename, self.filename, type,self.rate, self.hits,
                            (self.lines - self.hits), self.lines ))
        coberturaFile.write('''
        			<class name="%s" filename="%s" line-rate="%0.3f">'''
                            % ( self.name, self.filename, self.rate ))

        for key in sorted(self.covLines):
            coberturaFile.write('''
					<line number="%s" hits="%d"/>'''
                                % ( key, self.covLines[key] ))
        coberturaFile.write('''
	        		</class>''')

#-----------------------------------------------
class CovReport:
    def __init__(self,directories,output,low,high):
        self.covFiles={}
        self.directories = directories
        self.xmlFiles={}
        self.output = output
        self.low = low
        self.high = high

    def generateIndexHtml(self):
        # Generate the top-level 'index.html' file
        outputFileName=os.path.join(self.output,'index.html')
        outputFile = open(outputFileName,'w')
        outputFile.write('''
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
  <title id="window-title">???</title>
  <link rel="stylesheet" href="data/tablesorter-theme.css">
  <link rel="stylesheet" type="text/css" href="data/bcov.css"/>
</head>
<body>

<!-- Import Tempo etc -->
<script type="text/javascript" src="index.json"></script>
<script type="text/javascript" src="data/js/jquery.min.js"></script>
<script type="text/javascript" src="data/js/tablesorter.min.js"></script>
<script type="text/javascript" src="data/js/jquery.tablesorter.widgets.min.js"></script>
<script type="text/javascript" src="data/js/tempo.min.js"></script>
<script type="text/javascript" src="data/js/kcov.js"></script>

<table width="100%" border="0" cellspacing="0" cellpadding="0">
  <tr><td class="title">Aggregate Coverage Report</td></tr>
  <tr><td class="ruler"><img src="data/glass.png" width="3" height="3" alt=""/></td></tr>
  <tr>
    <td width="100%">
      <table cellpadding="1" border="0" width="100%">
        <tr id="command">
          <td class="headerItem" width="20%">Command:</td>
          <td id="header-command" class="headerValue" width="80%" colspan=6>???</td>
        </tr>
        <tr>
          <td class="headerItem" width="20%">Date: </td>
          <td id="header-date" class="headerValue" width="15%"></td>
          <td width="5%"></td>
          <td class="headerItem" width="20%">Instrumented lines:</td>
          <td id="header-instrumented" class="headerValue" width="10%">???</td>
        </tr>
        <tr>
          <td class="headerItem" width="20%">Code covered:</td>
          <td id="header-percent-covered" width="15%">???</td>
          <td width="5%"></td>
          <td class="headerItem" width="20%">Executed lines:</td>
          <td id="header-covered" class="headerValue" width="10%">???</td>
        </tr>
      </table>
    </td>
  </tr>
  <tr><td class="ruler"><img src="data/glass.png" width="3" height="3" alt=""/></td></tr>
</table>

<center>
  <table width="80%" cellpadding="2" cellspacing="1" border="0" id="index-table" class="tablesorter">
	<thead>
    <tr>
      <th class="tableHead" width="50%">Filename</th>
      <th width="20%">Coverage percent</th>
      <th width="10%">Covered lines</th>
      <th width="10%">Uncovered lines</th>
      <th width="10%">Executable lines</th>
    </tr>
    </thead>
    <tbody id="main-data">
    <tr data-template>
      <td class="coverFile">{{summary_name}}</td>
      <td class="coverPer"><span style="display:block;width:{{covered}}%" class="{{covered_class}}">{{covered}}%</td>
      <td class="coverNum">{{covered_lines}}</td>
      <td class="coverNum">{{uncovered_lines}}</td>
      <td class="coverNum">{{total_lines}}</td>
    </tr>
    </tbody>
    <tbody tablesorter-no-sort id="merged-data">
    <tr data-template>
      <td class="coverFile"><a href="{{link}}" title="{{title}}">{{summary_name}}</a></td>
      <td class="coverPer"><span style="display:block;width:{{covered}}%" class="{{covered_class}}">{{covered}}%</td>
      <td class="coverNum">{{covered_lines}}</td>
      <td class="coverNum">{{uncovered_lines}}</td>
      <td class="coverNum">{{total_lines}}</td>
    </tr>
    </tbody>
  </table>
</center>
<br>
<table width="100%" border="0" cellspacing="0" cellpadding="0">
  <tr><td class="ruler"><img src="data/amber.png" width="3" height="3" alt=""/></td></tr>
  <tr><td class="versionInfo">Generated by: <a href="http://simonkagstrom.github.com/kcov/index.html">Kcov</a></td></tr>
</table>
</body>
</html>
''')
        outputFile.close()

    def generateCoverageReport(self):

        for directory in self.directories:
            print ('Looking at directory %s' % directory)
            self.xmlFiles = [os.path.join(dp, f) for dp, dn, filenames in os.walk(directory) for f in filenames if os.path.splitext(f)[1] == '.xml']

            for filename in self.xmlFiles:
                doc = parseFile(filename)

                if doc is None:
                    print ('file not found')
                    sys.exit(2)
                root = doc.documentElement
                classes = root.getElementsByTagName('classes')

                for classesElt in classes:
                    for classElt in classesElt.childNodes:
                        if classElt.nodeType == Node.ELEMENT_NODE:
                            name = classElt.attributes['name'].value
                            classfile = classElt.attributes['filename'].value

                            classParser=None
                            if name in self.covFiles:
                                classParser=self.covFiles[name]
                            else:
                                self.covFiles[name]=CovFile(name,classfile,self.low,self.high)
                                classParser=self.covFiles[name]

                            classParser.parse(classElt)

        # Remove the output directory if it exists
        if os.path.isdir(self.output):
            shutil.rmtree(self.output)

        # Copy the 'data' directory tree from the first kcov output directory
        src = os.path.join(self.directories[0],'data')
        dst = os.path.join(self.output,'data')
        shutil.copytree(src,dst)

        # Generate the 'index.html' file
        self.generateIndexHtml()

        # Create the 'index.json' file
        indexFileName=os.path.join(self.output,'index.json')
        indexFile = open(indexFileName,'w')

        # Create the 'cobertura.xml' file
        coberturaFileName=os.path.join(self.output,'cobertura.xml')
        coberturaFile = open(coberturaFileName,'w')

        totalLines = 0
        totalHits = 0
        totalCov = 0.0

        # Calculate coverage for all Cobertura XML files and output this to the
        # 'index.json' file
        for key in self.covFiles:
            c = self.covFiles[key]
            c.calculateCoverage()
            totalLines = totalLines + c.lines
            totalHits = totalHits + c.hits

        totalCov = (float(totalHits) / float(totalLines)) * 100
        # print ('Total hits %d Total lines %d Total coverage %0.1f%%' % (totalHits, totalLines, totalCov))

        timeStr=time.strftime('%Y-%m-%d %H:%M:%S')

        indexFile.write('var percent_low = %d; var percent_high = %d;\n' % (self.low,self.high))
        indexFile.write("var header = { 'command' : 'Aggregate', 'date' : '%s', 'instrumented' : %d, 'covered' : %d,};\n"
                         % (timeStr,totalLines, totalHits))
        indexFile.write("var data = [\n")

        coberturaFile.write('''
<?xml version="1.0" ?>
<!DOCTYPE coverage SYSTEM 'http://cobertura.sourceforge.net/xml/coverage-03.dtd'>
<coverage line-rate="%0.3f" version="1.9" timestamp="%d">
	<packages>
        	<package name="aggregate" line-rate="%0.3f" branch-rate="1.0" complexity="1.0">
                	<classes>'''
                            % (totalCov, int(time.time()), totalCov))

        # Output individual coverate information for each Cobertura XML file
        for key in self.covFiles:
            c = self.covFiles[key]
            c.displayCoverage(indexFile,coberturaFile)

        # Close the 'index.json' file
        indexFile.write("];\n")
        indexFile.write("var merged_data = [];\n")
        indexFile.close()

        # Close the 'cobertura.xml' file
        coberturaFile.write('''
        	</classes>
        </packages>
</coverage>
''')
        coberturaFile.close()

        print ('Aggregate coverage report generated in %s' % self.output)
        
        

#-----------------------------------------------

class Main:
    def __init__(self):
        self.covFiles={}        

    def main(self,argv):

        parser = OptionParser(usage="%prog [options] outdir dir1 [dir2 ... dirn]", version="%prog 1.0")
        parser.add_option("-l", "--limits", dest="limits", default="25,75",
                          help="setup limits for low/high coverage (default 25,75)", metavar="LOW,HIGH")

        (options, args) = parser.parse_args()

        # Output directory and at least 1 kcov output directory must be specified
        if len(args) < 2:
            print ('output directory and at least 1 kcov output directory not specified')
            sys.exit(1)


        outputDir = args[0]
        args.remove(outputDir)
        directories = args        
        limits = options.limits

        low=25
        high=75
        sp=limits.split(',')

        try:
            if len(sp) == 2:
                low = int(sp[0])
                high = int(sp[1])
        except:
            print ('Invalid coverage limits specified')
            sys.exit(1)

        report = CovReport(directories,outputDir,low,high)
        report.generateCoverageReport()

if __name__ == "__main__":
    Main().main(sys.argv)
