#!/usr/local/bin/python
#CHIPSEC: Platform Security Assessment Framework
#Copyright (c) 2010-2015, Intel Corporation
# 
#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 2.
#
#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, write to the Free Software
#Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
#
#Contact information:
#chipsec@intel.com
#



import time
import sys
import traceback
import os
from os.path import basename

import xml.etree.ElementTree as ET
import platform
import xml.dom.minidom


class xmlAux:
    """Used to represent the variables to handle the xml output."""
    def __init__(self):
        """The Constructor."""
        self.test_cases = []
        self.useXML     = False
        self.testCase   = None
        self.class_name = None
        self.xmlFile    = None
        self.xmlStdout  = ""
        self.xmlStderr  = None
        self.properties = []

    def add_test_suite_property(self, name, value):
        """Adds a <property> child node to the <testsuite>."""
        if name is not None and value is not None:
            self.properties.append( ts_property( str(name).strip(), str(value).strip() ) )

    def set_xml_file(self, name):
        """Sets the filename used for the XML output."""
        if name != None:
            self.useXML = True
            self.xmlFile = name

    def append_stdout(self,msg):
        self.xmlStdout += str(msg) + "\n"

    def _check_testCase_exist(self):
        if self.testCase is None:
            if self.class_name is None:
                self.testCase = xmlTestCase( "test name", "class.name" )
            else:
                self.testCase = xmlTestCase( self.class_name, self.class_name )

    def _end_test(self):
        try:
            self.testCase.set_time()
            self.testCase.add_stdout_info( self.xmlStdout )
            self.test_cases.append( self.testCase )
            self.testCase = None
        except:
            print "Unexpected error:", sys.exc_info() [0]
            raise

    def passed_check(self):
        """Used when you want to mark a testcase as PASS and add it to the testsuite."""
        if self.useXML == True:
            self._check_testCase_exist()
            self._end_test()

    def failed_check(self, text):
        """Used when you want to mark a testcase as FAILURE and add it to the testsuite."""
        if self.useXML == True:
            self._check_testCase_exist()
            self.testCase.add_failure_info( text, None )
            self._end_test()

    def error_check(self, text):
        """Used when you want to mark a testcase as ERROR and add it to the testsuite."""
        if self.useXML == True:
            self._check_testCase_exist()
            self.testCase.add_error_info( text, None )
            self._end_test()

    def skipped_check(self, text):
        """Used when you want to mark a testcase as SKIPPED and add it to the testsuite."""
        if self.useXML == True:
            self._check_testCase_exist()
            self.testCase.add_skipped_info( text, None )
            self._end_test()

    def start_test(self, test_name):
        """Starts the test/testcase."""
        self.xmlStdout = ""
        if self.useXML == True:
            self.testCase = xmlTestCase( test_name, self.class_name )

    def start_module( self, module_name ):
        """Logs the start point of a Test, this is used for XML output.
           If XML file was not specified, it will just display a banner for the test name.
        """
        if self.useXML == True:
            self.class_name = module_name
            if self.testCase is not None:
                #If there's a test that did not send a status, so mark it as passed.
                self.passed_check( )
        self.xmlStdout = ""

    def end_module( self, module_name ):
        if self.useXML == True:
            self.class_name = ""
            if self.testCase is not None:
                #If there's a test that did not send a status, so mark it as passed.
                self.passed_check( )
        self.xmlStdout = ""

    def saveXML( self ):
        """Saves the XML info to a file in a JUnit style."""
        if not self.useXML or self.xmlFile is None: return False
        filename = self.xmlFile.replace("'", "")
        ts = xmlTestSuite( basename( os.path.splitext(filename)[0] ) )
        ts.test_cases = self.test_cases
        if self.properties is not None and len( self.properties ) > 0: ts.properties = self.properties
        ts.to_file( filename )
        print "[CHIPSEC] Saving output to XML file: %s" % str( os.path.abspath( filename ) )
        return True

class testCaseType:
    """Used to represent the types of TestCase that can be assigned (FAILURE, ERROR, SKIPPED, PASS)"""
    FAILURE = 1
    ERROR   = 2
    SKIPPED = 3
    PASS    = 4

class xmlTestCase():
    """Represents a JUnit test case with a result and possibly some stdout or stderr"""

    def __init__(self, name, classname, pTime=None, stdout=None, stderr=None, tcType=None, message=None, output=None):
        """The Constructor"""
        self.name      = name
        self.time      = None
        self.startTime = time.time()
        self.endTime   = None
        if pTime is not None:
            self.time  = pTime
        self.stdout    = stdout
        self.stderr    = stderr
        self.classname = classname
        self.tcType    = tcType
        self.tcMessage = message
        self.tcOutput  = output
        #Just to be compatible with junit_xml
        self.error_message   = ""
        self.error_output    = ""
        self.failure_message = ""
        self.failure_output  = ""
        self.skipped_message = ""
        self.skipped_output  = ""

        if tcType == testCaseType.ERROR:
            self.error_message = message
            self.error_output  = output
        elif tcType == testCaseType.FAILURE:
            self.failure_message = message
            self.failure_output  = output
        elif tcType == testCaseType.SKIPPED:
            self.skipped_message = message
            self.skipped_output  = output
        else:
            #Then it should be PASSED.
            self.tcType = testCaseType.PASS

    def is_skipped(self):
        """Returns True if the testCase is of Type Skipped, if not returns False"""
        if self.tcType == testCaseType.SKIPPED:
            return True
        else:
            False

    def is_error(self):
        """Returns True if the testCase is of Type Error, if not returns False"""
        if self.tcType == testCaseType.ERROR:
            return True
        else:
            False

    def is_failure(self):
        """Returns True if the testCase is of Type Failure, if not returns False"""
        if self.tcType == testCaseType.FAILURE:
            return True
        else:
            False

    def is_pass(self):
        """Returns True if the testCase is of Type Pass, if not returns False."""
        if self.tcType not in [testCaseType.ERROR, testCaseType.FAILURE, testCaseType.SKIPPED] or self.tcType == testCaseType.PASS:
            return True
        else:
            False

    def add_failure_info(self, message=None, output=None):
        """Sets the values for the corresponding Type Failure."""
        self.tcType          = testCaseType.FAILURE
        self.tcMessage       = message
        self.tcOutput        = output
        #To be compatible with junit_xml
        self.failure_message = message
        self.failure_output  = output

    def add_error_info(self, message=None, output=None):
        """Sets the values for the corresponding Type Error."""
        self.tcType        = testCaseType.ERROR
        self.tcMessage     = message
        self.tcOutput      = output
        #To be compatible with junit_xml
        self.error_message = message
        self.error_output  = output

    def add_skipped_info(self, message=None, output=None):
        """Sets the values for the corresponding Type Skipped."""
        self.tcType          = testCaseType.SKIPPED
        self.tcMessage       = message
        self.tcOutput        = output
        #To be compatible with junit_xml
        self.skipped_message = message
        self.skipped_output  = output

    def add_stdout_info(self, text):
        """Adds the text that is going to be part of the stdout for the TestCase."""
        if self.stdout is not None:
            self.stdout += str(text)
        else:
            self.stdout = str(text)

    def add_stderr_info(self, text):
        """Adds the text that is going to be part of the stderr for the TestCase."""
        if self.stderr is not None:
            self.stderr += str(text)
        else:
            self.stderr = str(text)

    def set_time(self, pTime=None):
        """Sets the time"""
        if pTime is not None:
            self.time = pTime
        else:
            self.endTime = time.time()
            self.time = self.endTime - self.startTime


class xmlTestSuite(object):
    """Suite of test cases, it's the father node for TestCase."""

    def __init__(self, name, test_cases=None, hostname=None, ts_id=None, package=None, timestamp=None, properties=None):
        """The Constructor."""
        self.name       = name
        if not test_cases:
            test_cases  = []
        self.test_cases = test_cases
        self.hostname   = hostname
        self.ts_id      = ts_id
        self.package    = package
        self.timestamp  = timestamp
        self.properties = properties

    def to_xml_string(self):
        """Returns the string representation of the JUnit XML document."""
        try:
            iter( self.test_cases )
        except TypeError:
            raise Exception('test_suite has no test cases')

        strXML = TestSuite.to_xml_string( TestSuite(self.name,       self.test_cases,
                                                     self.hostname,  self.ts_id, self.package,
                                                     self.timestamp, self.properties)
                                          )
        return strXML

    def to_file(self, file_name):
        """Writes the JUnit XML document to a file.
           In case of any error, it will print the exception information.
        """
        with open( file_name, 'wb') as f :
            #f.write( '<?xml-stylesheet type="text/xsl" href="junit.xsl"?>' )
            f.write( self.to_xml_string() )


class ts_property(object):
    """Class to represent a TestSuite property."""
    def __init__(self, name, value):
        """The constructor."""
        self.name  = name
        self.value = value


class TestSuite(object):
    """Suite of test cases"""

    def __init__(self, name, test_cases, hostname, ts_id, package, timestamp, properties):
        self.name       = name
        if not test_cases:
            test_cases  = []
        try:
            iter( test_cases )
        except:
            pass
        self.test_cases = test_cases
        self.hostname   = hostname
        self.ts_id      = ts_id
        self.package    = package
        self.timestamp  = timestamp
        if not properties:
            self.properties = []
        else:
            self.properties = properties


    def build_xml(self):
        """Builds the XML elements."""
        ts_attributes                  = dict()
        if self.name:
            ts_attributes["name"]      = str( self.name )
        else:
            ts_attributes["name"]      = "name"
        if self.hostname:
            ts_attributes["hostname"]  = str( self.hostname )
        if self.ts_id:
            ts_attributes["id"]        = str( self.ts_id )
        if self.package:
            ts_attributes["package"]   = str( self.package )
        if self.timestamp:
            ts_attributes["timestamp"] = str( self.timestamp )

        ts_attributes['failures']      = str( len( [tc for tc in self.test_cases if tc.is_failure()] ) )
        ts_attributes['errors']        = str( len( [tc for tc in self.test_cases if tc.is_error()] ) )
        ts_attributes['skipped']       = str( len( [tc for tc in self.test_cases if tc.is_skipped()] ) )
        #ts_attributes["time"]          = str( sum( [tc.time for tc in self.test_cases if tc.time] ) )
        ts_attributes["time"]          = "%.5f" % sum( [tc.time for tc in self.test_cases if tc.time] )
        ts_attributes["tests"]         = str( len( self.test_cases ) )

        xml_element = ET.Element( "testsuite", ts_attributes )

        if len(self.properties) > 0:
            ps_element = ET.SubElement( xml_element, "properties" )
            temp = dict()
            for p in self.properties:
                temp["name"]  = p.name
                temp["value"] = p.value
                py_element = ET.SubElement( ps_element, "property", temp )

        for tc in self.test_cases:
            tc_attributes = dict()
            tc_attributes['name'] = str( tc.name )
            if tc.time:
                tc_attributes['time'] = "%.5f" % tc.time
            if tc.classname:
                tc_attributes['classname'] = str( tc.classname )

            tc_element = ET.SubElement( xml_element, "testcase", tc_attributes )

            #For the is_pass() case, just log a 'pass' tag.
            if tc.is_pass():
                pass_element = ET.SubElement( tc_element, "pass", {'type':'pass'} )
            elif tc.is_failure():
                failure_element = ET.SubElement( tc_element, "failure", {'type': 'failure'} )
                if tc.failure_message:
                    failure_element.set( 'message', tc.failure_message )
                if tc.failure_output:
                    failure_element.text = tc.failure_output
            elif tc.is_error():
                error_element = ET.SubElement( tc_element, "error", {'type': 'error'} )
                if tc.error_message:
                    error_element.set( 'message', tc.error_message )
                if tc.error_output:
                    error_element.text = tc.error_output
            elif tc.is_skipped():
                skipped_element = ET.SubElement( tc_element, "skipped", {'type': 'skipped'} )
                if tc.skipped_message:
                    skipped_element.set( 'message', tc.skipped_message )
                if tc.skipped_output:
                    skipped_element.text = tc.skipped_output

            #system-out and system-err are common for all, so here we go.
            if tc.stdout:
                stdout_element = ET.SubElement( tc_element, "system-out" )
                stdout_element.text = tc.stdout
            if tc.stderr:
                stderr_element = ET.SubElement( tc_element, "system-err" )
                stderr_element.text = tc.stderr

        return xml_element


    def to_xml_string(self):
        """Returns a string representation of the XML Tree for the TestSuite."""
        xml_element  = ET.Element("testsuites")
        xml_element2 = self.build_xml()
        xml_element.append( xml_element2 )
        xml_string = ET.tostring( xml_element, None, None )

        if platform.system().lower() in ["windows", "linux"]:
            xml_string = xml.dom.minidom.parseString(xml_string).toprettyxml()

        return xml_string
