diff options
Diffstat (limited to '')
-rwxr-xr-x | src/VBox/ValidationKit/testdriver/reporter.py | 1984 |
1 files changed, 1984 insertions, 0 deletions
diff --git a/src/VBox/ValidationKit/testdriver/reporter.py b/src/VBox/ValidationKit/testdriver/reporter.py new file mode 100755 index 00000000..e5c55fd0 --- /dev/null +++ b/src/VBox/ValidationKit/testdriver/reporter.py @@ -0,0 +1,1984 @@ +# -*- coding: utf-8 -*- +# $Id: reporter.py $ +# pylint: disable=too-many-lines + +""" +Testdriver reporter module. +""" + +from __future__ import print_function; + +__copyright__ = \ +""" +Copyright (C) 2010-2023 Oracle and/or its affiliates. + +This file is part of VirtualBox base platform packages, as +available from https://www.virtualbox.org. + +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, in version 3 of the +License. + +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, see <https://www.gnu.org/licenses>. + +The contents of this file may alternatively be used under the terms +of the Common Development and Distribution License Version 1.0 +(CDDL), a copy of it is provided in the "COPYING.CDDL" file included +in the VirtualBox distribution, in which case the provisions of the +CDDL are applicable instead of those of the GPL. + +You may elect to license modified versions of this file under the +terms and conditions of either the GPL or the CDDL or both. + +SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 +""" +__version__ = "$Revision: 155244 $" + + +# Standard Python imports. +import array +import datetime +import errno +import gc +import os +import os.path +import sys +import time +import threading +import traceback + +# Validation Kit imports. +from common import utils; + +## test reporter instance +g_oReporter = None # type: ReporterBase +g_sReporterName = None; + + +class ReporterLock(object): + """ + Work around problem with garbage collection triggering __del__ method with + logging while inside the logger lock and causing a deadlock. + """ + + def __init__(self, sName): + self.sName = sName; + self.oLock = threading.RLock(); + self.oOwner = None; + self.cRecursion = 0; + self.fRestoreGC = False; + + def acquire(self): + """ Acquire the lock. """ + oSelf = threading.current_thread(); + + # Take the lock. + if not self.oLock.acquire(): # pylint: disable=consider-using-with + return False; + + self.oOwner = oSelf; + self.cRecursion += 1; + + # Disable GC to avoid __del__ w/ log statement randomly reenter the logger. + if self.cRecursion == 1: + self.fRestoreGC = gc.isenabled(); + if self.fRestoreGC: + gc.disable(); + + return True; + + def release(self): + """ Release the lock. """ + oSelf = threading.current_thread(); + + # Check the ownership. + if oSelf != self.oOwner: + raise threading.ThreadError(); + + # Drop one recursion. + self.cRecursion -= 1; + if self.cRecursion <= 0: + + # Final recursion. Clear owner and re-enable GC. + self.oOwner = None; + if self.fRestoreGC: + self.fRestoreGC = False; + gc.enable(); + + self.oLock.release(); + +## Reporter lock. +g_oLock = ReporterLock('reporter'); + + + +class PythonLoggingStream(object): + """ + Python logging => testdriver/reporter.py stream. + """ + + def write(self, sText): + """Writes python log message to our stream.""" + if g_oReporter is not None: + sText = sText.rstrip("\r\n"); + #g_oReporter.log(0, 'python: %s' % (sText), utils.getCallerName(), utils.getTimePrefix()); + return True; + + def flush(self): + """Flushes the stream.""" + return True; + + +class ReporterBase(object): + """ + Base class for the reporters. + """ + + def __init__(self): + self.iVerbose = 1; + self.iDebug = 0; + self.cErrors = 0; + self.fTimedOut = False; # Once set, it trickles all the way up. + self.atTests = []; + self.sName = os.path.splitext(os.path.basename(sys.argv[0]))[0]; + + # Hook into the python logging. + import logging; + logging.basicConfig(stream = PythonLoggingStream(), + level = logging.DEBUG, + format = '%(name)-12s %(levelname)-8s %(message)s'); + # + # Introspection and configuration. + # + + def isLocal(self): + """Is this a local reporter?""" + return False; + + def incVerbosity(self): + """Increases the verbosity level.""" + self.iVerbose += 1; + + def incDebug(self): + """Increases the debug level.""" + self.iDebug += 1; + + def getVerbosity(self): + """Returns the current verbosity level.""" + return self.iVerbose; + + def getDebug(self): + """Returns the current debug level.""" + return self.iDebug; + + def appendToProcessName(self, sAppend): + """ + Appends sAppend to the base process name. + Returns the new process name. + """ + self.sName = os.path.splitext(os.path.basename(sys.argv[0]))[0] + sAppend; + return self.sName; + + + # + # Generic logging. + # + + def log(self, iLevel, sText, sCaller, sTsPrf): + """ + Writes the specfied text to the log if iLevel is less or requal + to iVerbose. + """ + _ = iLevel; _ = sText; _ = sCaller; _ = sTsPrf; + return 0; + + # + # XML output from the reporter. + # + + def _xmlEscAttr(self, sValue): + """Escapes an XML attribute value.""" + sValue = sValue.replace('&', '&'); + sValue = sValue.replace('<', '<'); + sValue = sValue.replace('>', '>'); + #sValue = sValue.replace('\'', '''); + sValue = sValue.replace('"', '"'); + sValue = sValue.replace('\n', '
'); + sValue = sValue.replace('\r', '
'); + return sValue; + + def _xmlWrite(self, asText, fIndent = True): + """XML output function for the reporter.""" + _ = asText; _ = fIndent; + return None; + + def xmlFlush(self, fRetry = False, fForce = False): + """Flushes XML output if buffered.""" + _ = fRetry; _ = fForce; + return True; + + # + # XML output from child. + # + + def subXmlStart(self, oFileWrapper): + """Called by the file wrapper when the first bytes are written to the test pipe.""" + _ = oFileWrapper; + return None; + + def subXmlWrite(self, oFileWrapper, sRawXml, sCaller): + """Called by the file wrapper write method for test pipes.""" + return self.log(0, 'raw xml%s: %s' % (oFileWrapper.sPrefix, sRawXml), sCaller, utils.getTimePrefix()); + + def subXmlEnd(self, oFileWrapper): + """Called by the file wrapper __del__ method for test pipes.""" + _ = oFileWrapper; + return None; + + # + # File output. + # + + def addLogFile(self, oSrcFile, sSrcFilename, sAltName, sDescription, sKind, sCaller, sTsPrf): + """ + Adds the file to the report. + Returns True on success, False on failure. + """ + _ = oSrcFile; _ = sSrcFilename; _ = sAltName; _ = sDescription; _ = sKind; _ = sCaller; _ = sTsPrf; + return True; + + def addLogString(self, sLog, sLogName, sDescription, sKind, sCaller, sTsPrf): + """ + Adds the file to the report. + Returns True on success, False on failure. + """ + _ = sLog; _ = sLogName; _ = sDescription; _ = sKind; _ = sCaller; _ = sTsPrf; + return True; + + # + # Test reporting + # + + def _testGetFullName(self): + """ + Mangles the test names in atTest into a single name to make it easier + to spot where we are. + """ + sName = ''; + for t in self.atTests: + if sName != '': + sName += ', '; + sName += t[0]; + return sName; + + def testIncErrors(self): + """Increates the error count.""" + self.cErrors += 1; + return self.cErrors; + + def testSetTimedOut(self): + """Sets time out indicator for the current test and increases the error counter.""" + self.fTimedOut = True; + self.cErrors += 1; + return None; + + def testStart(self, sName, sCaller): + """ Starts a new test, may be nested. """ + (sTsPrf, sTsIso) = utils.getTimePrefixAndIsoTimestamp(); + self._xmlWrite([ '<Test timestamp="%s" name="%s">' % (sTsIso, self._xmlEscAttr(sName),), ]); + self.atTests.append((sName, self.cErrors, self.fTimedOut)); + self.fTimedOut = False; + return self.log(1, ' %-50s: TESTING' % (self._testGetFullName()), sCaller, sTsPrf); + + def testValue(self, sName, sValue, sUnit, sCaller): + """ Reports a benchmark value or something simiarlly useful. """ + (sTsPrf, sTsIso) = utils.getTimePrefixAndIsoTimestamp(); + self._xmlWrite([ '<Value timestamp="%s" name="%s" unit="%s" value="%s"/>' + % (sTsIso, self._xmlEscAttr(sName), self._xmlEscAttr(sUnit), self._xmlEscAttr(sValue)), ]); + return self.log(0, '** %-48s: %12s %s' % (sName, sValue, sUnit), sCaller, sTsPrf); + + def testFailure(self, sDetails, sCaller): + """ Reports a failure. """ + (sTsPrf, sTsIso) = utils.getTimePrefixAndIsoTimestamp(); + self.cErrors = self.cErrors + 1; + self._xmlWrite([ '<FailureDetails timestamp="%s" text="%s"/>' % (sTsIso, self._xmlEscAttr(sDetails),), ]); + return self.log(0, sDetails, sCaller, sTsPrf); + + def testDone(self, fSkipped, sCaller): + """ + Marks the current test as DONE, pops it and maks the next test on the + stack current. + Returns (name, errors). + """ + (sTsPrf, sTsIso) = utils.getTimePrefixAndIsoTimestamp(); + sFullName = self._testGetFullName(); + + # safe pop + if not self.atTests: + self.log(0, 'testDone on empty test stack!', sCaller, sTsPrf); + return ('internal error', 0); + fTimedOut = self.fTimedOut; + sName, cErrorsStart, self.fTimedOut = self.atTests.pop(); + + # log + xml. + cErrors = self.cErrors - cErrorsStart; + if cErrors == 0: + if fSkipped is not True: + self._xmlWrite([ ' <Passed timestamp="%s"/>' % (sTsIso,), '</Test>' ],); + self.log(1, '** %-50s: PASSED' % (sFullName,), sCaller, sTsPrf); + else: + self._xmlWrite([ ' <Skipped timestamp="%s"/>' % (sTsIso,), '</Test>' ]); + self.log(1, '** %-50s: SKIPPED' % (sFullName,), sCaller, sTsPrf); + elif fTimedOut: + self._xmlWrite([ ' <TimedOut timestamp="%s" errors="%d"/>' % (sTsIso, cErrors), '</Test>' ]); + self.log(0, '** %-50s: TIMED-OUT - %d errors' % (sFullName, cErrors), sCaller, sTsPrf); + else: + self._xmlWrite([ ' <Failed timestamp="%s" errors="%d"/>' % (sTsIso, cErrors), '</Test>' ]); + self.log(0, '** %-50s: FAILED - %d errors' % (sFullName, cErrors), sCaller, sTsPrf); + + # Flush buffers when reaching the last test. + if not self.atTests: + self.xmlFlush(fRetry = True); + + return (sName, cErrors); + + def testErrorCount(self): + """ + Returns the number of errors accumulated by the current test. + """ + cTests = len(self.atTests); + if cTests <= 0: + return self.cErrors; + return self.cErrors - self.atTests[cTests - 1][1]; + + def testCleanup(self, sCaller): + """ + Closes all open test as failed. + Returns True if no open tests, False if there were open tests. + """ + if not self.atTests: + return True; + for _ in range(len(self.atTests)): + self.testFailure('Test not closed by test drver', sCaller) + self.testDone(False, sCaller); + return False; + + # + # Misc. + # + + def doPollWork(self, sDebug = None): + """ + Check if any pending stuff expired and needs doing. + """ + _ = sDebug; + return None; + + + + +class LocalReporter(ReporterBase): + """ + Local reporter instance. + """ + + def __init__(self): + ReporterBase.__init__(self); + self.oLogFile = None; + self.oXmlFile = None; + self.fXmlOk = True; + self.iSubXml = 0; + self.iOtherFile = 0; + self.fnGetIsoTimestamp = utils.getIsoTimestamp; # Hack to get a timestamp in __del__. + self.oStdErr = sys.stderr; # Hack for __del__ output. + + # + # Figure the main log directory. + # + try: + self.sDefLogDir = os.path.abspath(os.path.expanduser(os.path.join('~', 'VBoxTestLogs'))); + except: + self.sDefLogDir = os.path.abspath("VBoxTestLogs"); + try: + sLogDir = os.path.abspath(os.environ.get('TESTBOX_REPORTER_LOG_DIR', self.sDefLogDir)); + if not os.path.isdir(sLogDir): + os.makedirs(sLogDir, 0o750); + except: + sLogDir = self.sDefLogDir; + if not os.path.isdir(sLogDir): + os.makedirs(sLogDir, 0o750); + + # + # Make a subdirectory for this test run. + # + sTs = datetime.datetime.utcnow().strftime('%Y-%m-%dT%H-%M-%S.log'); + self.sLogDir = sLogDir = os.path.join(sLogDir, '%s-%s' % (sTs, self.sName)); + try: + os.makedirs(self.sLogDir, 0o750); + except: + self.sLogDir = '%s-%s' % (self.sLogDir, os.getpid()); + os.makedirs(self.sLogDir, 0o750); + + # + # Open the log file and write a header. + # + sLogName = os.path.join(self.sLogDir, 'testsuite.log'); + sTsIso = utils.getIsoTimestamp(); + if sys.version_info[0] >= 3: # Add 'b' to prevent write taking issue with encode('utf-8') not returning a string. + self.oLogFile = utils.openNoInherit(sLogName, "wb"); + else: + self.oLogFile = utils.openNoInherit(sLogName, "w"); + self.oLogFile.write(('Created log file at %s.\nRunning: %s' % (sTsIso, sys.argv)).encode('utf-8')); + + # + # Open the xml log file and write the mandatory introduction. + # + # Note! This is done here and not in the base class because the remote + # logger doesn't really need this. It doesn't need the outer + # test wrapper either. + # + sXmlName = os.path.join(self.sLogDir, 'testsuite.xml'); + if sys.version_info[0] >= 3: # Add 'b' to prevent write taking issue with encode('utf-8') not returning a string. + self.oXmlFile = utils.openNoInherit(sXmlName, "wb"); + else: + self.oXmlFile = utils.openNoInherit(sXmlName, "w"); + self._xmlWrite([ '<?xml version="1.0" encoding="UTF-8" ?>', + '<Test timestamp="%s" name="%s">' % (sTsIso, self._xmlEscAttr(self.sName),), ], + fIndent = False); + + def __del__(self): + """Ends and completes the log files.""" + try: sTsIso = self.fnGetIsoTimestamp(); + except Exception as oXcpt: + sTsIso = str(oXcpt); + + if self.oLogFile is not None: + try: + self.oLogFile.write(('\nThe End %s\n' % (sTsIso,)).encode('utf-8')); + self.oLogFile.close(); + except: pass; + self.oLogFile = None; + + if self.oXmlFile is not None: + self._closeXml(sTsIso); + self.oXmlFile = None; + + def _closeXml(self, sTsIso): + """Closes the XML file.""" + if self.oXmlFile is not None: + # pop the test stack + while self.atTests: + sName, cErrorsStart, self.fTimedOut = self.atTests.pop(); + self._xmlWrite([ '<End timestamp="%s" errors="%d"/>' % (sTsIso, self.cErrors - cErrorsStart,), + '</%s>' % (sName,), ]); + + # The outer one is not on the stack. + self._xmlWrite([ ' <End timestamp="%s"/>' % (sTsIso,), + '</Test>', ], fIndent = False); + try: + self.oXmlFile.close(); + self.oXmlFile = None; + except: + pass; + + def _xmlWrite(self, asText, fIndent = True): + """Writes to the XML file.""" + for sText in asText: + if fIndent: + sIndent = ''.ljust((len(self.atTests) + 1) * 2); + sText = sIndent + sText; + sText += '\n'; + + try: + self.oXmlFile.write(sText.encode('utf-8')); + except: + if self.fXmlOk: + traceback.print_exc(); + self.fXmlOk = False; + return False; + return True; + + # + # Overridden methods. + # + + def isLocal(self): + """Is this a local reporter?""" + return True; + + def log(self, iLevel, sText, sCaller, sTsPrf): + if iLevel <= self.iVerbose: + # format it. + if self.iDebug <= 0: + sLogText = '%s %s' % (sTsPrf, sText); + elif self.iDebug <= 1: + sLogText = '%s %30s: %s' % (sTsPrf, sCaller, sText); + else: + sLogText = '%s e=%u %30s: %s' % (sTsPrf, self.cErrors, sCaller, sText); + + # output it. + if sys.version_info[0] >= 3: + sAscii = sLogText; + else: + sAscii = sLogText.encode('ascii', 'replace'); + if self.iDebug == 0: + print('%s: %s' % (self.sName, sAscii), file = self.oStdErr); + else: + print('%s' % (sAscii), file = self.oStdErr); + sLogText += '\n'; + try: + self.oLogFile.write(sLogText.encode('utf-8')); + except: + pass; + return 0; + + def addLogFile(self, oSrcFile, sSrcFilename, sAltName, sDescription, sKind, sCaller, sTsPrf): + # Figure the destination filename. + iOtherFile = self.iOtherFile; + self.iOtherFile += 1; + sDstFilename = os.path.join(self.sLogDir, 'other-%d-%s.log' \ + % (iOtherFile, os.path.splitext(os.path.basename(sSrcFilename))[0])); + self.log(0, '** Other log file: %s - %s (%s)' % (sDstFilename, sDescription, sSrcFilename), sCaller, sTsPrf); + + # Open the destination file and copy over the data. + fRc = True; + try: + oDstFile = utils.openNoInherit(sDstFilename, 'wb'); + except Exception as oXcpt: + self.log(0, 'error opening %s: %s' % (sDstFilename, oXcpt), sCaller, sTsPrf); + else: + while True: + try: + abBuf = oSrcFile.read(65536); + except Exception as oXcpt: + fRc = False; + self.log(0, 'error reading %s: %s' % (sSrcFilename, oXcpt), sCaller, sTsPrf); + else: + try: + oDstFile.write(abBuf); + except Exception as oXcpt: + fRc = False; + self.log(0, 'error writing %s: %s' % (sDstFilename, oXcpt), sCaller, sTsPrf); + else: + if abBuf: + continue; + break; + oDstFile.close(); + + # Leave a mark in the XML log. + self._xmlWrite(['<LogFile timestamp="%s" filename="%s" source="%s" kind="%s" ok="%s">%s</LogFile>\n' + % (utils.getIsoTimestamp(), self._xmlEscAttr(os.path.basename(sDstFilename)), self._xmlEscAttr(sSrcFilename), \ + self._xmlEscAttr(sKind), fRc, self._xmlEscAttr(sDescription))] ); + _ = sAltName; + return fRc; + + def addLogString(self, sLog, sLogName, sDescription, sKind, sCaller, sTsPrf): + # Figure the destination filename. + iOtherFile = self.iOtherFile; + self.iOtherFile += 1; + sDstFilename = os.path.join(self.sLogDir, 'other-%d-%s.log' \ + % (iOtherFile, os.path.splitext(os.path.basename(sLogName))[0])); + self.log(0, '** Other log file: %s - %s (%s)' % (sDstFilename, sDescription, sLogName), sCaller, sTsPrf); + + # Open the destination file and copy over the data. + fRc = True; + try: + oDstFile = utils.openNoInherit(sDstFilename, 'w'); + except Exception as oXcpt: + self.log(0, 'error opening %s: %s' % (sDstFilename, oXcpt), sCaller, sTsPrf); + else: + try: + oDstFile.write(sLog); + except Exception as oXcpt: + fRc = False; + self.log(0, 'error writing %s: %s' % (sDstFilename, oXcpt), sCaller, sTsPrf); + + oDstFile.close(); + + # Leave a mark in the XML log. + self._xmlWrite(['<LogFile timestamp="%s" filename="%s" source="%s" kind="%s" ok="%s">%s</LogFile>\n' + % (utils.getIsoTimestamp(), self._xmlEscAttr(os.path.basename(sDstFilename)), self._xmlEscAttr(sLogName), \ + self._xmlEscAttr(sKind), fRc, self._xmlEscAttr(sDescription))] ); + return fRc; + + def subXmlStart(self, oFileWrapper): + # Open a new file and just include it from the main XML. + iSubXml = self.iSubXml; + self.iSubXml += 1; + sSubXmlName = os.path.join(self.sLogDir, 'sub-%d.xml' % (iSubXml,)); + try: + oFileWrapper.oSubXmlFile = utils.openNoInherit(sSubXmlName, "w"); + except: + errorXcpt('open(%s)' % oFileWrapper.oSubXmlName); + oFileWrapper.oSubXmlFile = None; + else: + self._xmlWrite(['<Include timestamp="%s" filename="%s"/>\n' + % (utils.getIsoTimestamp(), self._xmlEscAttr(os.path.basename(sSubXmlName)))]); + return None; + + def subXmlWrite(self, oFileWrapper, sRawXml, sCaller): + if oFileWrapper.oSubXmlFile is not None: + try: + oFileWrapper.oSubXmlFile.write(sRawXml); + except: + pass; + if sCaller is None: pass; # pychecker - NOREF + return None; + + def subXmlEnd(self, oFileWrapper): + if oFileWrapper.oSubXmlFile is not None: + try: + oFileWrapper.oSubXmlFile.close(); + oFileWrapper.oSubXmlFile = None; + except: + pass; + return None; + + + +class RemoteReporter(ReporterBase): + """ + Reporter that talks to the test manager server. + """ + + + ## The XML sync min time (seconds). + kcSecXmlFlushMin = 30; + ## The XML sync max time (seconds). + kcSecXmlFlushMax = 120; + ## The XML sync idle time before flushing (seconds). + kcSecXmlFlushIdle = 5; + ## The XML sync line count threshold. + kcLinesXmlFlush = 512; + + ## The retry timeout. + kcSecTestManagerRetryTimeout = 120; + ## The request timeout. + kcSecTestManagerRequestTimeout = 30; + + + def __init__(self): + ReporterBase.__init__(self); + self.sTestManagerUrl = os.environ.get('TESTBOX_MANAGER_URL'); + self.sTestBoxUuid = os.environ.get('TESTBOX_UUID'); + self.idTestBox = int(os.environ.get('TESTBOX_ID')); + self.idTestSet = int(os.environ.get('TESTBOX_TEST_SET_ID')); + self._asXml = []; + self._secTsXmlFlush = utils.timestampSecond(); + self._secTsXmlLast = self._secTsXmlFlush; + self._fXmlFlushing = False; + self.oOutput = sys.stdout; # Hack for __del__ output. + self.fFlushEachLine = True; + self.fDebugXml = 'TESTDRIVER_REPORTER_DEBUG_XML' in os.environ; + + # Prepare the TM connecting. + from common import constants; + if sys.version_info[0] >= 3: + import urllib; + self._fnUrlEncode = urllib.parse.urlencode; # pylint: disable=no-member + self._fnUrlParseQs = urllib.parse.parse_qs; # pylint: disable=no-member + self._oParsedTmUrl = urllib.parse.urlparse(self.sTestManagerUrl); # pylint: disable=no-member + import http.client as httplib; # pylint: disable=no-name-in-module,import-error + else: + import urllib; + self._fnUrlEncode = urllib.urlencode; # pylint: disable=no-member + import urlparse; # pylint: disable=import-error + self._fnUrlParseQs = urlparse.parse_qs; # pylint: disable=no-member + self._oParsedTmUrl = urlparse.urlparse(self.sTestManagerUrl); # pylint: disable=no-member + import httplib; # pylint: disable=no-name-in-module,import-error + + if sys.version_info[0] >= 3 \ + or (sys.version_info[0] == 2 and sys.version_info[1] >= 6): + if self._oParsedTmUrl.scheme == 'https': # pylint: disable=no-member + self._fnTmConnect = lambda: httplib.HTTPSConnection(self._oParsedTmUrl.hostname, + timeout = self.kcSecTestManagerRequestTimeout); + else: + self._fnTmConnect = lambda: httplib.HTTPConnection( self._oParsedTmUrl.hostname, + timeout = self.kcSecTestManagerRequestTimeout); + else: + if self._oParsedTmUrl.scheme == 'https': # pylint: disable=no-member + self._fnTmConnect = lambda: httplib.HTTPSConnection(self._oParsedTmUrl.hostname); + else: + self._fnTmConnect = lambda: httplib.HTTPConnection( self._oParsedTmUrl.hostname); + self._dHttpHeader = \ + { + 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8', + 'User-Agent': 'TestDriverReporter/%s.0 (%s, %s)' % (__version__, utils.getHostOs(), utils.getHostArch(),), + 'Accept': 'text/plain,application/x-www-form-urlencoded', + 'Accept-Encoding': 'identity', + 'Cache-Control': 'max-age=0', + #'Connection': 'keep-alive', + }; + + dParams = { + constants.tbreq.ALL_PARAM_TESTBOX_UUID: self.sTestBoxUuid, + constants.tbreq.ALL_PARAM_TESTBOX_ID: self.idTestBox, + constants.tbreq.RESULT_PARAM_TEST_SET_ID: self.idTestSet, + }; + self._sTmServerPath = '/%s/testboxdisp.py?%s' \ + % ( self._oParsedTmUrl.path.strip('/'), # pylint: disable=no-member + self._fnUrlEncode(dParams), ); + + def __del__(self): + """Flush pending log messages?""" + if self._asXml: + self._xmlDoFlush(self._asXml, fRetry = True, fDtor = True); + + def _writeOutput(self, sText): + """ Does the actual writing and flushing. """ + if sys.version_info[0] >= 3: + print(sText, file = self.oOutput); + else: + print(sText.encode('ascii', 'replace'), file = self.oOutput); + if self.fFlushEachLine: self.oOutput.flush(); + return None; + + # + # Talking to TM. + # + + def _processTmStatusResponse(self, oConn, sOperation, fClose = True): + """ + Processes HTTP reponse from the test manager. + Returns True, False or None. None should be retried, the others not. + May raise exception on HTTP issue (retry ok). + """ + if sys.version_info[0] >= 3: import http.client as httplib; # pylint: disable=no-name-in-module,import-error + else: import httplib; # pylint: disable=import-error + from common import constants; + + # Read the response and (optionally) close the connection. + oResponse = oConn.getresponse(); + try: + sRspBody = oResponse.read(); + except httplib.IncompleteRead as oXcpt: + self._writeOutput('%s: %s: Warning: httplib.IncompleteRead: %s [expected %s, got %s]' + % (utils.getTimePrefix(), sOperation, oXcpt, oXcpt.expected, len(oXcpt.partial),)); + sRspBody = oXcpt.partial; + if fClose is True: + try: oConn.close(); + except: pass; + + # Make sure it's a string which encoding we grok. + if hasattr(sRspBody, 'decode'): + sRspBody = sRspBody.decode('utf-8', 'ignore'); + + # Check the content type. + sContentType = oResponse.getheader('Content-Type'); + if sContentType is not None and sContentType == 'application/x-www-form-urlencoded; charset=utf-8': + + # Parse the body and check the RESULT parameter. + dResponse = self._fnUrlParseQs(sRspBody, strict_parsing = True); + sResult = dResponse.get(constants.tbresp.ALL_PARAM_RESULT, None); + if isinstance(sResult, list): + sResult = sResult[0] if len(sResult) == 1 else '%d results' % (len(sResult),); + + if sResult is not None: + if sResult == constants.tbresp.STATUS_ACK: + return True; + if sResult == constants.tbresp.STATUS_NACK: + self._writeOutput('%s: %s: Failed (%s). (dResponse=%s)' + % (utils.getTimePrefix(), sOperation, sResult, dResponse,)); + return False; + + self._writeOutput('%s: %s: Failed - dResponse=%s' % (utils.getTimePrefix(), sOperation, dResponse,)); + else: + self._writeOutput('%s: %s: Unexpected Content-Type: %s' % (utils.getTimePrefix(), sOperation, sContentType,)); + self._writeOutput('%s: %s: Body: %s' % (utils.getTimePrefix(), sOperation, sRspBody,)); + return None; + + def _doUploadFile(self, oSrcFile, sSrcFilename, sDescription, sKind, sMime): + """ Uploads the given file to the test manager. """ + + # Prepare header and url. + dHeader = dict(self._dHttpHeader); + dHeader['Content-Type'] = 'application/octet-stream'; + self._writeOutput('%s: _doUploadFile: sHeader=%s' % (utils.getTimePrefix(), dHeader,)); + oSrcFile.seek(0, 2); + cbFileSize = oSrcFile.tell(); + self._writeOutput('%s: _doUploadFile: size=%d' % (utils.getTimePrefix(), cbFileSize,)); + oSrcFile.seek(0); + + if cbFileSize <= 0: # The Test Manager will bitch if the file size is 0, so skip uploading. + self._writeOutput('%s: _doUploadFile: Empty file, skipping upload' % utils.getTimePrefix()); + return False; + + from common import constants; + sUrl = self._sTmServerPath + '&' \ + + self._fnUrlEncode({ constants.tbreq.UPLOAD_PARAM_NAME: os.path.basename(sSrcFilename), + constants.tbreq.UPLOAD_PARAM_DESC: sDescription, + constants.tbreq.UPLOAD_PARAM_KIND: sKind, + constants.tbreq.UPLOAD_PARAM_MIME: sMime, + constants.tbreq.ALL_PARAM_ACTION: constants.tbreq.UPLOAD, + }); + + # Retry loop. + secStart = utils.timestampSecond(); + while True: + try: + oConn = self._fnTmConnect(); + oConn.request('POST', sUrl, oSrcFile.read(), dHeader); + fRc = self._processTmStatusResponse(oConn, '_doUploadFile', fClose = True); + oConn.close(); + if fRc is not None: + return fRc; + except: + logXcpt('warning: exception during UPLOAD request'); + + if utils.timestampSecond() - secStart >= self.kcSecTestManagerRetryTimeout: + self._writeOutput('%s: _doUploadFile: Timed out.' % (utils.getTimePrefix(),)); + break; + try: oSrcFile.seek(0); + except: + logXcpt(); + break; + self._writeOutput('%s: _doUploadFile: Retrying...' % (utils.getTimePrefix(), )); + time.sleep(2); + + return False; + + def _doUploadString(self, sSrc, sSrcName, sDescription, sKind, sMime): + """ Uploads the given string as a separate file to the test manager. """ + + # Prepare header and url. + dHeader = dict(self._dHttpHeader); + dHeader['Content-Type'] = 'application/octet-stream'; + self._writeOutput('%s: _doUploadString: sHeader=%s' % (utils.getTimePrefix(), dHeader,)); + self._writeOutput('%s: _doUploadString: size=%d' % (utils.getTimePrefix(), sys.getsizeof(sSrc),)); + + from common import constants; + sUrl = self._sTmServerPath + '&' \ + + self._fnUrlEncode({ constants.tbreq.UPLOAD_PARAM_NAME: os.path.basename(sSrcName), + constants.tbreq.UPLOAD_PARAM_DESC: sDescription, + constants.tbreq.UPLOAD_PARAM_KIND: sKind, + constants.tbreq.UPLOAD_PARAM_MIME: sMime, + constants.tbreq.ALL_PARAM_ACTION: constants.tbreq.UPLOAD, + }); + + # Retry loop. + secStart = utils.timestampSecond(); + while True: + try: + oConn = self._fnTmConnect(); + oConn.request('POST', sUrl, sSrc, dHeader); + fRc = self._processTmStatusResponse(oConn, '_doUploadString', fClose = True); + oConn.close(); + if fRc is not None: + return fRc; + except: + logXcpt('warning: exception during UPLOAD request'); + + if utils.timestampSecond() - secStart >= self.kcSecTestManagerRetryTimeout: + self._writeOutput('%s: _doUploadString: Timed out.' % (utils.getTimePrefix(),)); + break; + self._writeOutput('%s: _doUploadString: Retrying...' % (utils.getTimePrefix(), )); + time.sleep(2); + + return False; + + def _xmlDoFlush(self, asXml, fRetry = False, fDtor = False): + """ + The code that does the actual talking to the server. + Used by both xmlFlush and __del__. + """ + secStart = utils.timestampSecond(); + while True: + fRc = None; + try: + # Post. + from common import constants; + sPostBody = self._fnUrlEncode({constants.tbreq.XML_RESULT_PARAM_BODY: '\n'.join(asXml),}); + oConn = self._fnTmConnect(); + oConn.request('POST', + self._sTmServerPath + ('&%s=%s' % (constants.tbreq.ALL_PARAM_ACTION, constants.tbreq.XML_RESULTS)), + sPostBody, + self._dHttpHeader); + + fRc = self._processTmStatusResponse(oConn, '_xmlDoFlush', fClose = True); + if fRc is True: + if self.fDebugXml: + self._writeOutput('_xmlDoFlush:\n%s' % ('\n'.join(asXml),)); + return (None, False); + if fRc is False: + self._writeOutput('_xmlDoFlush: Failed - we should abort the test, really.'); + return (None, True); + except Exception as oXcpt: + if not fDtor: + logXcpt('warning: exception during XML_RESULTS request'); + else: + self._writeOutput('warning: exception during XML_RESULTS request: %s' % (oXcpt,)); + + if fRetry is not True \ + or utils.timestampSecond() - secStart >= self.kcSecTestManagerRetryTimeout: + break; + time.sleep(2); + + return (asXml, False); + + + # + # Overridden methods. + # + + def isLocal(self): + return False; + + def log(self, iLevel, sText, sCaller, sTsPrf): + if iLevel <= self.iVerbose: + if self.iDebug <= 0: + sLogText = '%s %s' % (sTsPrf, sText); + elif self.iDebug <= 1: + sLogText = '%s %30s: %s' % (sTsPrf, sCaller, sText); + else: + sLogText = '%s e=%u %30s: %s' % (sTsPrf, self.cErrors, sCaller, sText); + self._writeOutput(sLogText); + return 0; + + def addLogFile(self, oSrcFile, sSrcFilename, sAltName, sDescription, sKind, sCaller, sTsPrf): + fRc = True; + if sKind in [ 'text', 'log', 'process'] \ + or sKind.startswith('log/') \ + or sKind.startswith('info/') \ + or sKind.startswith('process/'): + self.log(0, '*** Uploading "%s" - KIND: "%s" - DESC: "%s" ***' + % (sSrcFilename, sKind, sDescription), sCaller, sTsPrf); + self.xmlFlush(); + g_oLock.release(); + try: + self._doUploadFile(oSrcFile, sAltName, sDescription, sKind, 'text/plain'); + finally: + g_oLock.acquire(); + elif sKind.startswith('screenshot/'): + self.log(0, '*** Uploading "%s" - KIND: "%s" - DESC: "%s" ***' + % (sSrcFilename, sKind, sDescription), sCaller, sTsPrf); + self.xmlFlush(); + g_oLock.release(); + try: + self._doUploadFile(oSrcFile, sAltName, sDescription, sKind, 'image/png'); + finally: + g_oLock.acquire(); + elif sKind.startswith('screenrecording/'): + self.log(0, '*** Uploading "%s" - KIND: "%s" - DESC: "%s" ***' + % (sSrcFilename, sKind, sDescription), sCaller, sTsPrf); + self.xmlFlush(); + g_oLock.release(); + try: + self._doUploadFile(oSrcFile, sAltName, sDescription, sKind, 'video/webm'); + finally: + g_oLock.acquire(); + elif sKind.startswith('misc/'): + self.log(0, '*** Uploading "%s" - KIND: "%s" - DESC: "%s" ***' + % (sSrcFilename, sKind, sDescription), sCaller, sTsPrf); + self.xmlFlush(); + g_oLock.release(); + try: + self._doUploadFile(oSrcFile, sAltName, sDescription, sKind, 'application/octet-stream'); + finally: + g_oLock.acquire(); + else: + self.log(0, '*** UNKNOWN FILE "%s" - KIND "%s" - DESC "%s" ***' + % (sSrcFilename, sKind, sDescription), sCaller, sTsPrf); + return fRc; + + def addLogString(self, sLog, sLogName, sDescription, sKind, sCaller, sTsPrf): + fRc = True; + if sKind in [ 'text', 'log', 'process'] \ + or sKind.startswith('log/') \ + or sKind.startswith('info/') \ + or sKind.startswith('process/'): + self.log(0, '*** Uploading "%s" - KIND: "%s" - DESC: "%s" ***' + % (sLogName, sKind, sDescription), sCaller, sTsPrf); + self.xmlFlush(); + g_oLock.release(); + try: + self._doUploadString(sLog, sLogName, sDescription, sKind, 'text/plain'); + finally: + g_oLock.acquire(); + else: + self.log(0, '*** UNKNOWN FILE "%s" - KIND "%s" - DESC "%s" ***' + % (sLogName, sKind, sDescription), sCaller, sTsPrf); + return fRc; + + def xmlFlush(self, fRetry = False, fForce = False): + """ + Flushes the XML back log. Called with the lock held, may leave it + while communicating with the server. + """ + if not self._fXmlFlushing: + asXml = self._asXml; + self._asXml = []; + if asXml or fForce is True: + self._fXmlFlushing = True; + + g_oLock.release(); + try: + (asXml, fIncErrors) = self._xmlDoFlush(asXml, fRetry = fRetry); + finally: + g_oLock.acquire(); + + if fIncErrors: + self.testIncErrors(); + + self._fXmlFlushing = False; + if asXml is None: + self._secTsXmlFlush = utils.timestampSecond(); + else: + self._asXml = asXml + self._asXml; + return True; + + self._secTsXmlFlush = utils.timestampSecond(); + return False; + + def _xmlFlushIfNecessary(self, fPolling = False, sDebug = None): + """Flushes the XML back log if necessary.""" + tsNow = utils.timestampSecond(); + cSecs = tsNow - self._secTsXmlFlush; + cSecsLast = tsNow - self._secTsXmlLast; + if fPolling is not True: + self._secTsXmlLast = tsNow; + + # Absolute flush thresholds. + if cSecs >= self.kcSecXmlFlushMax: + return self.xmlFlush(); + if len(self._asXml) >= self.kcLinesXmlFlush: + return self.xmlFlush(); + + # Flush if idle long enough. + if cSecs >= self.kcSecXmlFlushMin \ + and cSecsLast >= self.kcSecXmlFlushIdle: + return self.xmlFlush(); + + _ = sDebug; + return False; + + def _xmlWrite(self, asText, fIndent = True): + """XML output function for the reporter.""" + self._asXml += asText; + self._xmlFlushIfNecessary(); + _ = fIndent; # No pretty printing, thank you. + return None; + + def subXmlStart(self, oFileWrapper): + oFileWrapper.sXmlBuffer = ''; + return None; + + def subXmlWrite(self, oFileWrapper, sRawXml, sCaller): + oFileWrapper.sXmlBuffer += sRawXml; + _ = sCaller; + return None; + + def subXmlEnd(self, oFileWrapper): + sRawXml = oFileWrapper.sXmlBuffer; + ## @todo should validate the document here and maybe auto terminate things. Adding some hints to have the server do + # this instead. + g_oLock.acquire(); + try: + self._asXml += [ '<PushHint testdepth="%d"/>' % (len(self.atTests),), + sRawXml, + '<PopHint testdepth="%d"/>' % (len(self.atTests),),]; + self._xmlFlushIfNecessary(); + finally: + g_oLock.release(); + return None; + + def doPollWork(self, sDebug = None): + if self._asXml: + g_oLock.acquire(); + try: + self._xmlFlushIfNecessary(fPolling = True, sDebug = sDebug); + finally: + g_oLock.release(); + return None; + + +# +# Helpers +# + +g_fnComXcptFormatter = None; + +def setComXcptFormatter(fnCallback): + """ + Install callback for prettier COM exception formatting. + + The callback replaces the work done by format_exception_only() and + takes the same arguments. It returns None if not interested in the + exception. + """ + global g_fnComXcptFormatter; + g_fnComXcptFormatter = fnCallback; + return True; + +def formatExceptionOnly(oType, oXcpt, sCaller, sTsPrf): + """ + Wrapper around traceback.format_exception_only and __g_fnComXcptFormatter. + """ + #asRet = ['oType=%s type(oXcpt)=%s' % (oType, type(oXcpt),)]; + asRet = []; + + # Try the callback first. + fnCallback = g_fnComXcptFormatter; + if fnCallback: + try: + asRetCb = fnCallback(oType, oXcpt); + if asRetCb: + return asRetCb; + #asRet += asRetCb; + except: + g_oReporter.log(0, '** internal-error: Hit exception #2 in __g_fnComXcptFormatter! %s' + % (traceback.format_exc()), sCaller, sTsPrf); + asRet += ['internal error: exception in __g_fnComXcptFormatter']; + + # Now try format_exception_only: + try: + asRet += traceback.format_exception_only(oType, oXcpt); + except: + g_oReporter.log(0, '** internal-error: Hit exception #2 in format_exception_only! %s' + % (traceback.format_exc()), sCaller, sTsPrf); + asRet += ['internal error: Exception in format_exception_only!']; + return asRet; + + +def logXcptWorker(iLevel, fIncErrors, sPrefix="", sText=None, cFrames=1): + """ + Log an exception, optionally with a preceeding message and more than one + call frame. + """ + g_oLock.acquire(); + try: + + if fIncErrors: + g_oReporter.testIncErrors(); + + ## @todo skip all this if iLevel is too high! + + # Try get exception info. + sTsPrf = utils.getTimePrefix(); + try: + oType, oValue, oTraceback = sys.exc_info(); + except: + oType = oValue = oTraceback = None; + if oType is not None: + + # Try format the info + try: + rc = 0; + sCaller = utils.getCallerName(oTraceback.tb_frame); + if sText is not None: + rc = g_oReporter.log(iLevel, "%s%s" % (sPrefix, sText), sCaller, sTsPrf); + asInfo = None; + try: + asInfo = formatExceptionOnly(oType, oValue, sCaller, sTsPrf); + atEntries = traceback.extract_tb(oTraceback); + atEntries.reverse(); + if cFrames is not None and cFrames <= 1: + if atEntries: + asInfo = asInfo + traceback.format_list(atEntries[:1]); + else: + asInfo.append('Traceback (stack order):') + if cFrames is not None and cFrames < len(atEntries): + asInfo = asInfo + traceback.format_list(atEntries[:cFrames]); + else: + asInfo = asInfo + traceback.format_list(atEntries); + asInfo.append('Stack:') + asInfo = asInfo + traceback.format_stack(oTraceback.tb_frame.f_back, cFrames); + except: + g_oReporter.log(0, '** internal-error: Hit exception #2! %s' % (traceback.format_exc()), sCaller, sTsPrf); + + if asInfo: + # Do the logging. + for sItem in asInfo: + asLines = sItem.splitlines(); + for sLine in asLines: + rc = g_oReporter.log(iLevel, '%s%s' % (sPrefix, sLine), sCaller, sTsPrf); + + else: + g_oReporter.log(iLevel, 'No exception info...', sCaller, sTsPrf); + rc = -3; + except: + g_oReporter.log(0, '** internal-error: Hit exception! %s' % (traceback.format_exc()), None, sTsPrf); + rc = -2; + else: + g_oReporter.log(0, '** internal-error: No exception! %s' + % (utils.getCallerName(iFrame=3)), utils.getCallerName(iFrame=3), sTsPrf); + rc = -1; + + finally: + g_oLock.release(); + return rc; + + +# +# The public Classes +# +class FileWrapper(object): + """ File like class for TXS EXEC and similar. """ + def __init__(self, sPrefix): + self.sPrefix = sPrefix; + + def __del__(self): + self.close(); + + def close(self): + """ file.close """ + # Nothing to be done. + return; + + def read(self, cb): + """file.read""" + _ = cb; + return ""; + + def write(self, sText): + """file.write""" + if not utils.isString(sText): + if isinstance(sText, array.array): + try: + if sys.version_info < (3, 9, 0): + # Removed since Python 3.9. + sText = sText.tostring(); # pylint: disable=no-member + else: + sText = sText.tobytes(); + except: + pass; + if hasattr(sText, 'decode'): + try: + sText = sText.decode('utf-8', 'ignore'); + except: + pass; + g_oLock.acquire(); + try: + sTsPrf = utils.getTimePrefix(); + sCaller = utils.getCallerName(); + asLines = sText.splitlines(); + for sLine in asLines: + g_oReporter.log(0, '%s: %s' % (self.sPrefix, sLine), sCaller, sTsPrf); + except: + traceback.print_exc(); + finally: + g_oLock.release(); + return None; + +class FileWrapperTestPipe(object): + """ + File like class for the test pipe (TXS EXEC and similar). + + This is also used to submit XML test result files. + """ + def __init__(self): + self.sPrefix = ''; + self.fStarted = False; + self.fClosed = False; + self.sTagBuffer = None; + self.cTestDepth = 0; + self.acTestErrors = []; + + def __del__(self): + self.close(); + + def close(self): + """ file.close """ + if self.fStarted is True and self.fClosed is False: + self.fClosed = True; + + # Close open <Test> elements: + if self.cTestDepth > 0: + sNow = utils.getIsoTimestamp() + cErrors = 0; + while self.cTestDepth > 0: + self.cTestDepth -= 1; + if self.acTestErrors: + cErrors += self.acTestErrors.pop(); + cErrors += 1; + g_oReporter.subXmlWrite(self, + '\n%s <Failed timestamp="%s" errors="%s"/>\n%s</Test>\n' + % (' ' * self.cTestDepth, sNow, cErrors, ' ' * self.cTestDepth), + utils.getCallerName()); + + # Tell the reporter that the XML input is done. + try: g_oReporter.subXmlEnd(self); + except: + try: traceback.print_exc(); + except: pass; + return True; + + def read(self, cb = None): + """file.read""" + _ = cb; + return ""; + + def write(self, sText): + """file.write""" + # lazy start. + if self.fStarted is not True: + try: + g_oReporter.subXmlStart(self); + except: + traceback.print_exc(); + self.fStarted = True; + + # Turn non-string stuff into strings. + if not utils.isString(sText): + if isinstance(sText, array.array): + try: + if sys.version_info < (3, 9, 0): + # Removed since Python 3.9. + sText = sText.tostring(); # pylint: disable=no-member + else: + sText = sText.tobytes(); + except: + pass; + if hasattr(sText, 'decode'): + try: sText = sText.decode('utf-8', 'ignore'); + except: pass; + + try: + # + # Write the XML to the reporter. + # + g_oReporter.subXmlWrite(self, sText, utils.getCallerName()); + + # + # Parse the supplied text and look for <Failed.../> tags to keep track of the + # error counter. This is only a very lazy aproach. + # + idxText = 0; + while sText: + if self.sTagBuffer is None: + # Look for the start of a tag. + idxStart = sText.find('<', idxText); + if idxStart != -1: + # If the end was found inside the current buffer, parse the line, + # otherwise we have to save it for later. + idxEnd = sText.find('>', idxStart); + if idxEnd != -1: + self._processXmlElement(sText[idxStart:idxEnd+1]); + idxText = idxEnd; + else: + self.sTagBuffer = sText[idxStart:]; + break; + else: + break; + else: + # Search for the end of the tag and parse the whole tag. + assert(idxText == 0); + idxEnd = sText.find('>'); + if idxEnd != -1: + self._processXmlElement(self.sTagBuffer + sText[:idxEnd+1]); + self.sTagBuffer = None; + idxText = idxEnd; + else: + self.sTagBuffer = self.sTagBuffer + sText[idxText:]; + break; + except: + traceback.print_exc(); + return None; + + def _processXmlElement(self, sElement): + """ + Processes a complete XML tag. + + We handle the 'Failed' tag to keep track of the error counter. + We also track 'Test' tags to make sure we close with all of them properly closed. + """ + # Make sure we don't parse any space between < and the element name. + sElement = sElement.strip(); + + # Find the end of the name + idxEndName = sElement.find(' '); + if idxEndName == -1: + idxEndName = sElement.find('>'); + if idxEndName >= 0: + if sElement[idxEndName - 1] == '/': + idxEndName -= 1; + else: + idxEndName = len(sElement); + sElementName = sElement[1:idxEndName]; + + # <Failed>: + if sElementName == 'Failed': + g_oLock.acquire(); + try: + g_oReporter.testIncErrors(); + finally: + g_oLock.release(); + if self.acTestErrors: + self.acTestErrors[-1] += 1; # get errors attrib + # <Test> + elif sElementName == 'Test': + self.cTestDepth += 1; + self.acTestErrors.append(0); + # </Test> + elif sElementName == '/Test': + self.cTestDepth -= 1; + if self.acTestErrors: + cErrors = self.acTestErrors.pop(); + if self.acTestErrors: + self.acTestErrors[-1] += cErrors; + + +# +# The public APIs. +# + +def log(sText, sCaller = None): + """Writes the specfied text to the log.""" + g_oLock.acquire(); + try: + rc = g_oReporter.log(1, sText, sCaller if sCaller else utils.getCallerName(), utils.getTimePrefix()); + except: + rc = -1; + finally: + g_oLock.release(); + return rc; + +def logXcpt(sText=None, cFrames=1): + """ + Log an exception, optionally with a preceeding message and more than one + call frame. + """ + return logXcptWorker(1, False, "", sText, cFrames); + +def log2(sText, sCaller = None): + """Log level 2: Writes the specfied text to the log.""" + g_oLock.acquire(); + try: + rc = g_oReporter.log(2, sText, sCaller if sCaller else utils.getCallerName(), utils.getTimePrefix()); + except: + rc = -1; + finally: + g_oLock.release(); + return rc; + +def log2Xcpt(sText=None, cFrames=1): + """ + Log level 2: Log an exception, optionally with a preceeding message and + more than one call frame. + """ + return logXcptWorker(2, False, "", sText, cFrames); + +def log3(sText, sCaller = None): + """Log level 3: Writes the specfied text to the log.""" + g_oLock.acquire(); + try: + rc = g_oReporter.log(3, sText, sCaller if sCaller else utils.getCallerName(), utils.getTimePrefix()); + except: + rc = -1; + finally: + g_oLock.release(); + return rc; + +def log3Xcpt(sText=None, cFrames=1): + """ + Log level 3: Log an exception, optionally with a preceeding message and + more than one call frame. + """ + return logXcptWorker(3, False, "", sText, cFrames); + +def log4(sText, sCaller = None): + """Log level 4: Writes the specfied text to the log.""" + g_oLock.acquire(); + try: + rc = g_oReporter.log(4, sText, sCaller if sCaller else utils.getCallerName(), utils.getTimePrefix()); + except: + rc = -1; + finally: + g_oLock.release(); + return rc; + +def log4Xcpt(sText=None, cFrames=1): + """ + Log level 4: Log an exception, optionally with a preceeding message and + more than one call frame. + """ + return logXcptWorker(4, False, "", sText, cFrames); + +def log5(sText, sCaller = None): + """Log level 2: Writes the specfied text to the log.""" + g_oLock.acquire(); + try: + rc = g_oReporter.log(5, sText, sCaller if sCaller else utils.getCallerName(), utils.getTimePrefix()); + except: + rc = -1; + finally: + g_oLock.release(); + return rc; + +def log5Xcpt(sText=None, cFrames=1): + """ + Log level 5: Log an exception, optionally with a preceeding message and + more than one call frame. + """ + return logXcptWorker(5, False, "", sText, cFrames); + +def log6(sText, sCaller = None): + """Log level 6: Writes the specfied text to the log.""" + g_oLock.acquire(); + try: + rc = g_oReporter.log(6, sText, sCaller if sCaller else utils.getCallerName(), utils.getTimePrefix()); + except: + rc = -1; + finally: + g_oLock.release(); + return rc; + +def log6Xcpt(sText=None, cFrames=1): + """ + Log level 6: Log an exception, optionally with a preceeding message and + more than one call frame. + """ + return logXcptWorker(6, False, "", sText, cFrames); + +def maybeErr(fIsError, sText): + """ Maybe error or maybe normal log entry. """ + if fIsError is True: + return error(sText, sCaller = utils.getCallerName()); + return log(sText, sCaller = utils.getCallerName()); + +def maybeErrXcpt(fIsError, sText=None, cFrames=1): + """ Maybe error or maybe normal log exception entry. """ + if fIsError is True: + return errorXcpt(sText, cFrames); + return logXcpt(sText, cFrames); + +def maybeLog(fIsNotError, sText): + """ Maybe error or maybe normal log entry. """ + if fIsNotError is not True: + return error(sText, sCaller = utils.getCallerName()); + return log(sText, sCaller = utils.getCallerName()); + +def maybeLogXcpt(fIsNotError, sText=None, cFrames=1): + """ Maybe error or maybe normal log exception entry. """ + if fIsNotError is not True: + return errorXcpt(sText, cFrames); + return logXcpt(sText, cFrames); + +def error(sText, sCaller = None): + """ + Writes the specfied error message to the log. + + This will add an error to the current test. + + Always returns False for the convenience of methods returning boolean + success indicators. + """ + g_oLock.acquire(); + try: + g_oReporter.testIncErrors(); + g_oReporter.log(0, '** error: %s' % (sText), sCaller if sCaller else utils.getCallerName(), utils.getTimePrefix()); + except: + pass; + finally: + g_oLock.release(); + return False; + +def errorXcpt(sText=None, cFrames=1): + """ + Log an error caused by an exception. If sText is given, it will preceed + the exception information. cFrames can be used to display more stack. + + This will add an error to the current test. + + Always returns False for the convenience of methods returning boolean + success indicators. + """ + logXcptWorker(0, True, '** error: ', sText, cFrames); + return False; + +def errorTimeout(sText): + """ + Flags the current test as having timed out and writes the specified message to the log. + + This will add an error to the current test. + + Always returns False for the convenience of methods returning boolean + success indicators. + """ + g_oLock.acquire(); + try: + g_oReporter.testSetTimedOut(); + g_oReporter.log(0, '** timeout-error: %s' % (sText), utils.getCallerName(), utils.getTimePrefix()); + except: + pass; + finally: + g_oLock.release(); + return False; + +def fatal(sText): + """ + Writes a fatal error to the log. + + This will add an error to the current test. + + Always returns False for the convenience of methods returning boolean + success indicators. + """ + g_oLock.acquire(); + try: + g_oReporter.testIncErrors(); + g_oReporter.log(0, '** fatal error: %s' % (sText), utils.getCallerName(), utils.getTimePrefix()); + except: + pass + finally: + g_oLock.release(); + return False; + +def fatalXcpt(sText=None, cFrames=1): + """ + Log a fatal error caused by an exception. If sText is given, it will + preceed the exception information. cFrames can be used to display more + stack. + + This will add an error to the current test. + + Always returns False for the convenience of methods returning boolean + success indicators. + """ + logXcptWorker(0, True, "** fatal error: ", sText, cFrames); + return False; + +def addLogFile(sFilename, sKind, sDescription = '', sAltName = None): + """ + Adds the specified log file to the report if the file exists. + + The sDescription is a free form description of the log file. + + The sKind parameter is for adding some machine parsable hint what kind of + log file this really is. + + Returns True on success, False on failure (no ENOENT errors are logged). + """ + sTsPrf = utils.getTimePrefix(); + sCaller = utils.getCallerName(); + fRc = False; + if sAltName is None: + sAltName = sFilename; + + try: + oSrcFile = utils.openNoInherit(sFilename, 'rb'); + except IOError as oXcpt: + if oXcpt.errno != errno.ENOENT: + logXcpt('addLogFile(%s,%s,%s)' % (sFilename, sDescription, sKind)); + else: + logXcpt('addLogFile(%s,%s,%s) IOError' % (sFilename, sDescription, sKind)); + except: + logXcpt('addLogFile(%s,%s,%s)' % (sFilename, sDescription, sKind)); + else: + g_oLock.acquire(); + try: + fRc = g_oReporter.addLogFile(oSrcFile, sFilename, sAltName, sDescription, sKind, sCaller, sTsPrf); + finally: + g_oLock.release(); + oSrcFile.close(); + return fRc; + +def addLogString(sLog, sLogName, sKind, sDescription = ''): + """ + Adds the specified log string to the report. + + The sLog parameter sets the name of the log file. + + The sDescription is a free form description of the log file. + + The sKind parameter is for adding some machine parsable hint what kind of + log file this really is. + + Returns True on success, False on failure (no ENOENT errors are logged). + """ + sTsPrf = utils.getTimePrefix(); + sCaller = utils.getCallerName(); + fRc = False; + + g_oLock.acquire(); + try: + fRc = g_oReporter.addLogString(sLog, sLogName, sDescription, sKind, sCaller, sTsPrf); + finally: + g_oLock.release(); + return fRc; + +def isLocal(): + """Is this a local reporter?""" + return g_oReporter.isLocal() + +def incVerbosity(): + """Increases the verbosity level.""" + return g_oReporter.incVerbosity() + +def incDebug(): + """Increases the debug level.""" + return g_oReporter.incDebug() + +def getVerbosity(): + """Returns the current verbosity level.""" + return g_oReporter.getVerbosity() + +def getDebug(): + """Returns the current debug level.""" + return g_oReporter.getDebug() + +def appendToProcessName(sAppend): + """ + Appends sAppend to the base process name. + Returns the new process name. + """ + return g_oReporter.appendToProcessName(sAppend); + +def getErrorCount(): + """ + Get the current error count for the entire test run. + """ + g_oLock.acquire(); + try: + cErrors = g_oReporter.cErrors; + finally: + g_oLock.release(); + return cErrors; + +def doPollWork(sDebug = None): + """ + This can be called from wait loops and similar to make the reporter call + home with pending XML and such. + """ + g_oReporter.doPollWork(sDebug); + return None; + + +# +# Test reporting, a bit similar to RTTestI*. +# + +def testStart(sName): + """ + Starts a new test (pushes it). + """ + g_oLock.acquire(); + try: + rc = g_oReporter.testStart(sName, utils.getCallerName()); + finally: + g_oLock.release(); + return rc; + +def testValue(sName, sValue, sUnit): + """ + Reports a benchmark value or something simiarlly useful. + """ + g_oLock.acquire(); + try: + rc = g_oReporter.testValue(sName, str(sValue), sUnit, utils.getCallerName()); + finally: + g_oLock.release(); + return rc; + +def testFailure(sDetails): + """ + Reports a failure. + We count these calls and testDone will use them to report PASSED or FAILED. + + Returns False so that a return False line can be saved. + """ + g_oLock.acquire(); + try: + g_oReporter.testFailure(sDetails, utils.getCallerName()); + finally: + g_oLock.release(); + return False; + +def testFailureXcpt(sDetails = ''): + """ + Reports a failure with exception. + We count these calls and testDone will use them to report PASSED or FAILED. + + Returns False so that a return False line can be saved. + """ + # Extract exception info. + try: + oType, oValue, oTraceback = sys.exc_info(); + except: + oType = oValue, oTraceback = None; + if oType is not None: + sCaller = utils.getCallerName(oTraceback.tb_frame); + sXcpt = ' '.join(formatExceptionOnly(oType, oValue, sCaller, utils.getTimePrefix())); + else: + sCaller = utils.getCallerName(); + sXcpt = 'No exception at %s' % (sCaller,); + + # Use testFailure to do the work. + g_oLock.acquire(); + try: + if sDetails == '': + g_oReporter.testFailure('Exception: %s' % (sXcpt,), sCaller); + else: + g_oReporter.testFailure('%s: %s' % (sDetails, sXcpt), sCaller); + finally: + g_oLock.release(); + return False; + +def testDone(fSkipped = False): + """ + Completes the current test (pops it), logging PASSED / FAILURE. + + Returns a tuple with the name of the test and its error count. + """ + g_oLock.acquire(); + try: + rc = g_oReporter.testDone(fSkipped, utils.getCallerName()); + finally: + g_oLock.release(); + return rc; + +def testErrorCount(): + """ + Gets the error count of the current test. + + Returns the number of errors. + """ + g_oLock.acquire(); + try: + cErrors = g_oReporter.testErrorCount(); + finally: + g_oLock.release(); + return cErrors; + +def testCleanup(): + """ + Closes all open tests with a generic error condition. + + Returns True if no open tests, False if something had to be closed with failure. + """ + g_oLock.acquire(); + try: + fRc = g_oReporter.testCleanup(utils.getCallerName()); + g_oReporter.xmlFlush(fRetry = False, fForce = True); + finally: + g_oLock.release(); + fRc = False; + return fRc; + + +# +# Sub XML stuff. +# + +def addSubXmlFile(sFilename): + """ + Adds a sub-xml result file to the party. + """ + fRc = False; + try: + oSrcFile = utils.openNoInherit(sFilename, 'r'); + except IOError as oXcpt: + if oXcpt.errno != errno.ENOENT: + logXcpt('addSubXmlFile(%s)' % (sFilename,)); + except: + logXcpt('addSubXmlFile(%s)' % (sFilename,)); + else: + try: + oWrapper = FileWrapperTestPipe() + oWrapper.write(oSrcFile.read()); + oWrapper.close(); + except: + logXcpt('addSubXmlFile(%s)' % (sFilename,)); + oSrcFile.close(); + + return fRc; + + +# +# Other useful debugging tools. +# + +def logAllStacks(cFrames = None): + """ + Logs the stacks of all python threads. + """ + sTsPrf = utils.getTimePrefix(); + sCaller = utils.getCallerName(); + g_oLock.acquire(); + + cThread = 0; + for idThread, oStack in sys._current_frames().items(): # >=2.5, a bit ugly - pylint: disable=protected-access + try: + if cThread > 0: + g_oReporter.log(1, '', sCaller, sTsPrf); + g_oReporter.log(1, 'Thread %s (%#x)' % (idThread, idThread), sCaller, sTsPrf); + try: + asInfo = traceback.format_stack(oStack, cFrames); + except: + g_oReporter.log(1, ' Stack formatting failed w/ exception', sCaller, sTsPrf); + else: + for sInfo in asInfo: + asLines = sInfo.splitlines(); + for sLine in asLines: + g_oReporter.log(1, sLine, sCaller, sTsPrf); + except: + pass; + cThread += 1; + + g_oLock.release(); + return None; + +def checkTestManagerConnection(): + """ + Checks the connection to the test manager. + + Returns True if the connection is fine, False if not, None if not remote + reporter. + + Note! This as the sideeffect of flushing XML. + """ + g_oLock.acquire(); + try: + fRc = g_oReporter.xmlFlush(fRetry = False, fForce = True); + finally: + g_oLock.release(); + fRc = False; + return fRc; + +def flushall(fSkipXml = False): + """ + Flushes all output streams, both standard and logger related. + This may also push data to the remote test manager. + """ + try: sys.stdout.flush(); + except: pass; + try: sys.stderr.flush(); + except: pass; + + if fSkipXml is not True: + g_oLock.acquire(); + try: + g_oReporter.xmlFlush(fRetry = False); + finally: + g_oLock.release(); + + return True; + + +# +# Module initialization. +# + +def _InitReporterModule(): + """ + Instantiate the test reporter. + """ + global g_oReporter, g_sReporterName + + g_sReporterName = os.getenv("TESTBOX_REPORTER", "local"); + if g_sReporterName == "local": + g_oReporter = LocalReporter(); + elif g_sReporterName == "remote": + g_oReporter = RemoteReporter(); # Correct, but still plain stupid. pylint: disable=redefined-variable-type + else: + print(os.path.basename(__file__) + ": Unknown TESTBOX_REPORTER value: '" + g_sReporterName + "'", file = sys.stderr); + raise Exception("Unknown TESTBOX_REPORTER value '" + g_sReporterName + "'"); + +if __name__ != "checker": # pychecker avoidance. + _InitReporterModule(); |