From f215e02bf85f68d3a6106c2a1f4f7f063f819064 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Thu, 11 Apr 2024 10:17:27 +0200 Subject: Adding upstream version 7.0.14-dfsg. Signed-off-by: Daniel Baumann --- .../ValidationKit/testmanager/webui/wuigraphwiz.py | 660 +++++++++++++++++++++ 1 file changed, 660 insertions(+) create mode 100755 src/VBox/ValidationKit/testmanager/webui/wuigraphwiz.py (limited to 'src/VBox/ValidationKit/testmanager/webui/wuigraphwiz.py') diff --git a/src/VBox/ValidationKit/testmanager/webui/wuigraphwiz.py b/src/VBox/ValidationKit/testmanager/webui/wuigraphwiz.py new file mode 100755 index 00000000..040b3217 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/webui/wuigraphwiz.py @@ -0,0 +1,660 @@ +# -*- coding: utf-8 -*- +# $Id: wuigraphwiz.py $ + +""" +Test Manager WUI - Graph Wizard +""" + +__copyright__ = \ +""" +Copyright (C) 2012-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 . + +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 $" + +# Python imports. +import functools; + +# Validation Kit imports. +from testmanager.webui.wuimain import WuiMain; +from testmanager.webui.wuihlpgraph import WuiHlpLineGraphErrorbarY, WuiHlpGraphDataTableEx; +from testmanager.webui.wuireport import WuiReportBase; + +from common import utils, webutils; +from common import constants; + + +class WuiGraphWiz(WuiReportBase): + """Construct a graph for analyzing test results (values) across builds and testboxes.""" + + ## @name Series name parts. + ## @{ + kfSeriesName_TestBox = 1; + kfSeriesName_Product = 2; + kfSeriesName_Branch = 4; + kfSeriesName_BuildType = 8; + kfSeriesName_OsArchs = 16; + kfSeriesName_TestCase = 32; + kfSeriesName_TestCaseArgs = 64; + kfSeriesName_All = 127; + ## @} + + + def __init__(self, oModel, dParams, fSubReport = False, fnDPrint = None, oDisp = None): + WuiReportBase.__init__(self, oModel, dParams, fSubReport = fSubReport, fnDPrint = fnDPrint, oDisp = oDisp); + + # Select graph implementation. + if dParams[WuiMain.ksParamGraphWizImpl] == 'charts': + from testmanager.webui.wuihlpgraphgooglechart import WuiHlpLineGraphErrorbarY as MyGraph; + self.oGraphClass = MyGraph; + elif dParams[WuiMain.ksParamGraphWizImpl] == 'matplotlib': + from testmanager.webui.wuihlpgraphmatplotlib import WuiHlpLineGraphErrorbarY as MyGraph; + self.oGraphClass = MyGraph; + else: + self.oGraphClass = WuiHlpLineGraphErrorbarY; + + + # + def _figureSeriesNameBits(self, aoSeries): + """ Figures out the method (bitmask) to use when naming series. """ + if len(aoSeries) <= 1: + return WuiGraphWiz.kfSeriesName_TestBox; + + # Start with all and drop unnecessary specs one-by-one. + fRet = WuiGraphWiz.kfSeriesName_All; + + if [oSrs.idTestBox for oSrs in aoSeries].count(aoSeries[0].idTestBox) == len(aoSeries): + fRet &= ~WuiGraphWiz.kfSeriesName_TestBox; + + if [oSrs.idBuildCategory for oSrs in aoSeries].count(aoSeries[0].idBuildCategory) == len(aoSeries): + fRet &= ~WuiGraphWiz.kfSeriesName_Product; + fRet &= ~WuiGraphWiz.kfSeriesName_Branch; + fRet &= ~WuiGraphWiz.kfSeriesName_BuildType; + fRet &= ~WuiGraphWiz.kfSeriesName_OsArchs; + else: + if [oSrs.oBuildCategory.sProduct for oSrs in aoSeries].count(aoSeries[0].oBuildCategory.sProduct) == len(aoSeries): + fRet &= ~WuiGraphWiz.kfSeriesName_Product; + if [oSrs.oBuildCategory.sBranch for oSrs in aoSeries].count(aoSeries[0].oBuildCategory.sBranch) == len(aoSeries): + fRet &= ~WuiGraphWiz.kfSeriesName_Branch; + if [oSrs.oBuildCategory.sType for oSrs in aoSeries].count(aoSeries[0].oBuildCategory.sType) == len(aoSeries): + fRet &= ~WuiGraphWiz.kfSeriesName_BuildType; + + # Complicated. + fRet &= ~WuiGraphWiz.kfSeriesName_OsArchs; + daTestBoxes = {}; + for oSeries in aoSeries: + if oSeries.idTestBox in daTestBoxes: + daTestBoxes[oSeries.idTestBox].append(oSeries); + else: + daTestBoxes[oSeries.idTestBox] = [oSeries,]; + for aoSeriesPerTestBox in daTestBoxes.values(): + if len(aoSeriesPerTestBox) >= 0: + asOsArches = aoSeriesPerTestBox[0].oBuildCategory.asOsArches; + for i in range(1, len(aoSeriesPerTestBox)): + if aoSeriesPerTestBox[i].oBuildCategory.asOsArches != asOsArches: + fRet |= WuiGraphWiz.kfSeriesName_OsArchs; + break; + + if aoSeries[0].oTestCaseArgs is None: + fRet &= ~WuiGraphWiz.kfSeriesName_TestCaseArgs; + if [oSrs.idTestCase for oSrs in aoSeries].count(aoSeries[0].idTestCase) == len(aoSeries): + fRet &= ~WuiGraphWiz.kfSeriesName_TestCase; + else: + fRet &= ~WuiGraphWiz.kfSeriesName_TestCase; + if [oSrs.idTestCaseArgs for oSrs in aoSeries].count(aoSeries[0].idTestCaseArgs) == len(aoSeries): + fRet &= ~WuiGraphWiz.kfSeriesName_TestCaseArgs; + + return fRet; + + def _getSeriesNameFromBits(self, oSeries, fBits): + """ Creates a series name from bits (kfSeriesName_xxx). """ + assert fBits != 0; + sName = ''; + + if fBits & WuiGraphWiz.kfSeriesName_Product: + if sName: sName += ' / '; + sName += oSeries.oBuildCategory.sProduct; + + if fBits & WuiGraphWiz.kfSeriesName_Branch: + if sName: sName += ' / '; + sName += oSeries.oBuildCategory.sBranch; + + if fBits & WuiGraphWiz.kfSeriesName_BuildType: + if sName: sName += ' / '; + sName += oSeries.oBuildCategory.sType; + + if fBits & WuiGraphWiz.kfSeriesName_OsArchs: + if sName: sName += ' / '; + sName += ' & '.join(oSeries.oBuildCategory.asOsArches); + + if fBits & WuiGraphWiz.kfSeriesName_TestCaseArgs: + if sName: sName += ' / '; + if oSeries.idTestCaseArgs is not None: + sName += oSeries.oTestCase.sName + ':#' + str(oSeries.idTestCaseArgs); + else: + sName += oSeries.oTestCase.sName; + elif fBits & WuiGraphWiz.kfSeriesName_TestCase: + if sName: sName += ' / '; + sName += oSeries.oTestCase.sName; + + if fBits & WuiGraphWiz.kfSeriesName_TestBox: + if sName: sName += ' / '; + sName += oSeries.oTestBox.sName; + + return sName; + + def _calcGraphName(self, oSeries, fSeriesName, sSampleName): + """ Constructs a name for the graph. """ + fGraphName = ~fSeriesName & ( WuiGraphWiz.kfSeriesName_TestBox + | WuiGraphWiz.kfSeriesName_Product + | WuiGraphWiz.kfSeriesName_Branch + | WuiGraphWiz.kfSeriesName_BuildType + ); + sName = self._getSeriesNameFromBits(oSeries, fGraphName); + if sName: sName += ' - '; + sName += sSampleName; + return sName; + + def _calcSampleName(self, oCollection): + """ Constructs a name for a sample source (collection). """ + if oCollection.sValue is not None: + asSampleName = [oCollection.sValue, 'in',]; + elif oCollection.sType == self._oModel.ksTypeElapsed: + asSampleName = ['Elapsed time', 'for', ]; + elif oCollection.sType == self._oModel.ksTypeResult: + asSampleName = ['Error count', 'for',]; + else: + return 'Invalid collection type: "%s"' % (oCollection.sType,); + + sTestName = ', '.join(oCollection.asTests if oCollection.asTests[0] else oCollection.asTests[1:]); + if sTestName == '': + # Use the testcase name if there is only one for all series. + if not oCollection.aoSeries: + return asSampleName[0]; + if len(oCollection.aoSeries) > 1: + idTestCase = oCollection.aoSeries[0].idTestCase; + for oSeries in oCollection.aoSeries: + if oSeries.idTestCase != idTestCase: + return asSampleName[0]; + sTestName = oCollection.aoSeries[0].oTestCase.sName; + return ' '.join(asSampleName) + ' ' + sTestName; + + + def _splitSeries(self, aoSeries): + """ + Splits the data series (ReportGraphModel.DataSeries) into one or more graphs. + + Returns an array of data series arrays. + """ + # Must be at least two series for something to be splittable. + if len(aoSeries) <= 1: + if not aoSeries: + return []; + return [aoSeries,]; + + # Split on unit. + dUnitSeries = {}; + for oSeries in aoSeries: + if oSeries.iUnit not in dUnitSeries: + dUnitSeries[oSeries.iUnit] = []; + dUnitSeries[oSeries.iUnit].append(oSeries); + + # Sort the per-unit series since the build category was only sorted by ID. + for iUnit in dUnitSeries: + def mycmp(oSelf, oOther): + """ __cmp__ like function. """ + iCmp = utils.stricmp(oSelf.oBuildCategory.sProduct, oOther.oBuildCategory.sProduct); + if iCmp != 0: + return iCmp; + iCmp = utils.stricmp(oSelf.oBuildCategory.sBranch, oOther.oBuildCategory.sBranch); + if iCmp != 0: + return iCmp; + iCmp = utils.stricmp(oSelf.oBuildCategory.sType, oOther.oBuildCategory.sType); + if iCmp != 0: + return iCmp; + iCmp = utils.stricmp(oSelf.oTestBox.sName, oOther.oTestBox.sName); + if iCmp != 0: + return iCmp; + return 0; + dUnitSeries[iUnit] = sorted(dUnitSeries[iUnit], key = functools.cmp_to_key(mycmp)); + + # Split the per-unit series up if necessary. + cMaxPerGraph = self._dParams[WuiMain.ksParamGraphWizMaxPerGraph]; + aaoRet = []; + for aoUnitSeries in dUnitSeries.values(): + while len(aoUnitSeries) > cMaxPerGraph: + aaoRet.append(aoUnitSeries[:cMaxPerGraph]); + aoUnitSeries = aoUnitSeries[cMaxPerGraph:]; + if aoUnitSeries: + aaoRet.append(aoUnitSeries); + + return aaoRet; + + def _configureGraph(self, oGraph): + """ + Configures oGraph according to user parameters and other config settings. + + Returns oGraph. + """ + oGraph.setWidth(self._dParams[WuiMain.ksParamGraphWizWidth]) + oGraph.setHeight(self._dParams[WuiMain.ksParamGraphWizHeight]) + oGraph.setDpi(self._dParams[WuiMain.ksParamGraphWizDpi]) + oGraph.setErrorBarY(self._dParams[WuiMain.ksParamGraphWizErrorBarY]); + oGraph.setFontSize(self._dParams[WuiMain.ksParamGraphWizFontSize]); + if hasattr(oGraph, 'setXkcdStyle'): + oGraph.setXkcdStyle(self._dParams[WuiMain.ksParamGraphWizXkcdStyle]); + + return oGraph; + + def _generateInteractiveForm(self): + """ + Generates the HTML for the interactive form. + Returns (sTopOfForm, sEndOfForm) + """ + + # + # The top of the form. + # + sTop = '
\n' \ + ' \n' \ + ' \n' \ + % ( WuiMain.ksParamAction, WuiMain.ksActionGraphWiz, + WuiMain.ksParamGraphWizSrcTestSetId, self._dParams[WuiMain.ksParamGraphWizSrcTestSetId], + ); + + sTop += '
\n'; + sTop += ' \n' \ + % ( WuiMain.ksParamGraphWizWidth, ); + + # + # Top: First row. + # + sTop += '
\n'; + + # time. + sNow = self._dParams[WuiMain.ksParamEffectiveDate]; + if sNow is None: sNow = ''; + sTop += '
\n'; + sTop += ' \n' \ + ' \n' \ + % ( WuiMain.ksParamEffectiveDate, + WuiMain.ksParamEffectiveDate, WuiMain.ksParamEffectiveDate, sNow, ); + + sTop += ' \n' % ( WuiMain.ksParamReportPeriods, 1, ); + sTop += '
\n'; + + # Graph options top row. + sTop += '
\n'; + + # graph type. + sTop += ' \n' \ + ' \n'; + + # graph size. + sTop += ' \n' \ + ' x\n' \ + ' \n' \ + ' '\ + ' \n' \ + ' \n' \ + % ( WuiMain.ksParamGraphWizWidth, + WuiMain.ksParamGraphWizWidth, WuiMain.ksParamGraphWizWidth, self._dParams[WuiMain.ksParamGraphWizWidth], + WuiMain.ksParamGraphWizHeight, WuiMain.ksParamGraphWizHeight, self._dParams[WuiMain.ksParamGraphWizHeight], + WuiMain.ksParamGraphWizDpi, + WuiMain.ksParamGraphWizDpi, WuiMain.ksParamGraphWizDpi, self._dParams[WuiMain.ksParamGraphWizDpi], + webutils.escapeAttr('return graphwizSetDefaultSizeValues("graphwiz-nav", "%s", "%s", "%s");' + % ( WuiMain.ksParamGraphWizWidth, WuiMain.ksParamGraphWizHeight, + WuiMain.ksParamGraphWizDpi )), + ); + + sTop += '
\n'; # (options row 1) + + sTop += '
\n'; # (end of row 1) + + # + # Top: Second row. + # + sTop += '
\n'; + + # Submit + sFormButton = '\n'; + sTop += '
' + sFormButton + '
\n'; + + + # Options. + sTop += '
\n'; + + sTop += ' \n' \ + ' \n' \ + % ( WuiMain.ksParamGraphWizTabular, WuiMain.ksParamGraphWizTabular, + ' checked' if self._dParams[WuiMain.ksParamGraphWizTabular] else '', + WuiMain.ksParamGraphWizTabular); + + if hasattr(self.oGraphClass, 'setXkcdStyle'): + sTop += ' \n' \ + ' \n' \ + % ( WuiMain.ksParamGraphWizXkcdStyle, WuiMain.ksParamGraphWizXkcdStyle, + ' checked' if self._dParams[WuiMain.ksParamGraphWizXkcdStyle] else '', + WuiMain.ksParamGraphWizXkcdStyle); + elif self._dParams[WuiMain.ksParamGraphWizXkcdStyle]: + sTop += ' \n' \ + % ( WuiMain.ksParamGraphWizXkcdStyle, WuiMain.ksParamGraphWizXkcdStyle, ); + + if not hasattr(self.oGraphClass, 'kfNoErrorBarsSupport'): + sTop += ' \n' \ + ' \n' \ + ' \n' \ + ' \n' \ + % ( WuiMain.ksParamGraphWizErrorBarY, WuiMain.ksParamGraphWizErrorBarY, + ' checked' if self._dParams[WuiMain.ksParamGraphWizErrorBarY] else '', + 'Error bars shows some of the max and min results on the Y-axis.', + WuiMain.ksParamGraphWizErrorBarY, + WuiMain.ksParamGraphWizMaxErrorBarY, + WuiMain.ksParamGraphWizMaxErrorBarY, WuiMain.ksParamGraphWizMaxErrorBarY, + self._dParams[WuiMain.ksParamGraphWizMaxErrorBarY], + 'Maximum number of Y-axis error bar per graph. (Too many makes it unreadable.)' + ); + else: + if self._dParams[WuiMain.ksParamGraphWizErrorBarY]: + sTop += '\n' \ + % ( WuiMain.ksParamGraphWizErrorBarY, WuiMain.ksParamGraphWizErrorBarY, ); + sTop += '\n' \ + % ( WuiMain.ksParamGraphWizMaxErrorBarY, WuiMain.ksParamGraphWizMaxErrorBarY, + self._dParams[WuiMain.ksParamGraphWizMaxErrorBarY], ); + + sTop += ' \n' \ + ' \n' \ + % ( WuiMain.ksParamGraphWizFontSize, + WuiMain.ksParamGraphWizFontSize, WuiMain.ksParamGraphWizFontSize, + self._dParams[WuiMain.ksParamGraphWizFontSize], ); + + sTop += ' \n' \ + ' \n' \ + % ( WuiMain.ksParamGraphWizMaxPerGraph, + WuiMain.ksParamGraphWizMaxPerGraph, WuiMain.ksParamGraphWizMaxPerGraph, + self._dParams[WuiMain.ksParamGraphWizMaxPerGraph], + 'Max data series per graph.' ); + + sTop += '
\n'; # (options row 2) + + sTop += '
\n'; # (end of row 2) + + sTop += '
\n'; # end of top. + + # + # The end of the page selection. + # + sEnd = '
\n'; + + # + # Testbox selection + # + aidTestBoxes = list(self._dParams[WuiMain.ksParamGraphWizTestBoxIds]); + sEnd += '
\n' \ + '

TestBox Selection:

\n' \ + '
    \n'; + + # Get a list of eligible testboxes from the DB. + for oTestBox in self._oModel.getEligibleTestBoxes(): + try: aidTestBoxes.remove(oTestBox.idTestBox); + except: sChecked = ''; + else: sChecked = ' checked'; + sEnd += '
  1. ' \ + '
  2. \n' \ + % ( WuiMain.ksParamGraphWizTestBoxIds, oTestBox.idTestBox, oTestBox.idTestBox, sChecked, + oTestBox.idTestBox, oTestBox.sName); + + # List testboxes that have been checked in a different period or something. + for idTestBox in aidTestBoxes: + oTestBox = self._oModel.oCache.getTestBox(idTestBox); + sEnd += '
  3. ' \ + '
  4. \n' \ + % ( WuiMain.ksParamGraphWizTestBoxIds, oTestBox.idTestBox, oTestBox.idTestBox, + oTestBox.idTestBox, oTestBox.sName); + + sEnd += '
\n' \ + '
\n'; + + # + # Build category selection. + # + aidBuildCategories = list(self._dParams[WuiMain.ksParamGraphWizBuildCatIds]); + sEnd += '
\n' \ + '

Build Category Selection:

\n' \ + '
    \n'; + for oBuildCat in self._oModel.getEligibleBuildCategories(): + try: aidBuildCategories.remove(oBuildCat.idBuildCategory); + except: sChecked = ''; + else: sChecked = ' checked'; + sEnd += '
  1. ' \ + '
  2. \n' \ + % ( WuiMain.ksParamGraphWizBuildCatIds, oBuildCat.idBuildCategory, oBuildCat.idBuildCategory, sChecked, + oBuildCat.idBuildCategory, + oBuildCat.sProduct, oBuildCat.sBranch, oBuildCat.sType, ' & '.join(oBuildCat.asOsArches) ); + assert not aidBuildCategories; # SQL should return all currently selected. + + sEnd += '
\n' \ + '
\n'; + + # + # Testcase variations. + # + sEnd += '
\n' \ + '

Miscellaneous:

\n' \ + '
    '; + + sEnd += '
  1. \n' \ + ' \n' \ + ' \n' \ + '
  2. \n' \ + % ( WuiMain.ksParamGraphWizSepTestVars, WuiMain.ksParamGraphWizSepTestVars, + ' checked' if self._dParams[WuiMain.ksParamGraphWizSepTestVars] else '', + WuiMain.ksParamGraphWizSepTestVars ); + + + sEnd += '
  3. \n' \ + ' Test case ID:\n' \ + ' \n' \ + '
  4. \n' \ + % ( WuiMain.ksParamGraphWizTestCaseIds, + WuiMain.ksParamGraphWizTestCaseIds, WuiMain.ksParamGraphWizTestCaseIds, + ','.join([str(i) for i in self._dParams[WuiMain.ksParamGraphWizTestCaseIds]]), ); + + sEnd += '
\n' \ + '
\n'; + + #sEnd += '

 

\n'; + + # + # Finish up the form. + # + sEnd += '

' + sFormButton + '

\n'; + sEnd += '
\n' \ + '
\n'; + + return (sTop, sEnd); + + def generateReportBody(self): + fInteractive = not self._fSubReport; + + # Quick mockup. + self._sTitle = 'Graph Wizzard'; + + sHtml = ''; + sHtml += '

Incomplete code - no complaints yet, thank you!!

\n'; + + # + # Create a form for altering the data we're working with. + # + if fInteractive: + (sTopOfForm, sEndOfForm) = self._generateInteractiveForm(); + sHtml += sTopOfForm; + del sTopOfForm; + + # + # Emit the graphs. At least one per sample source. + # + sHtml += '
\n'; + iGraph = 0; + aoCollections = self._oModel.fetchGraphData(); + for iCollection, oCollection in enumerate(aoCollections): + # Name the graph and add a checkbox for removing it. + sSampleName = self._calcSampleName(oCollection); + sHtml += '
\n' % (iCollection,); + if fInteractive: + sHtml += '
\n' \ + ' \n' \ + ' \n' \ + '
\n' \ + % ( WuiMain.ksParamReportSubjectIds, WuiMain.ksParamReportSubjectIds, oCollection.sType, + ':'.join([str(idStr) for idStr in oCollection.aidStrTests]), + ':%u' % oCollection.idStrValue if oCollection.idStrValue else '', + WuiMain.ksParamReportSubjectIds, sSampleName ); + + if oCollection.aoSeries: + # + # Split the series into sub-graphs as needed and produce SVGs. + # + aaoSeries = self._splitSeries(oCollection.aoSeries); + for aoSeries in aaoSeries: + # Gather the data for this graph. (Most big stuff is passed by + # reference, so there shouldn't be any large memory penalty for + # repacking the data here.) + sYUnit = None; + if aoSeries[0].iUnit < len(constants.valueunit.g_asNames) and aoSeries[0].iUnit > 0: + sYUnit = constants.valueunit.g_asNames[aoSeries[0].iUnit]; + oData = WuiHlpGraphDataTableEx(sXUnit = 'Build revision', sYUnit = sYUnit); + + fSeriesName = self._figureSeriesNameBits(aoSeries); + for oSeries in aoSeries: + sSeriesName = self._getSeriesNameFromBits(oSeries, fSeriesName); + asHtmlTooltips = None; + if len(oSeries.aoRevInfo) == len(oSeries.aiRevisions): + asHtmlTooltips = []; + for i, oRevInfo in enumerate(oSeries.aoRevInfo): + sPlusMinus = ''; + if oSeries.acSamples[i] > 1: + sPlusMinus = ' (+%s/-%s; %u samples)' \ + % ( utils.formatNumber(oSeries.aiErrorBarAbove[i]), + utils.formatNumber(oSeries.aiErrorBarBelow[i]), + oSeries.acSamples[i]) + sTooltip = ''\ + '' \ + % ( sSeriesName, + utils.formatNumber(oSeries.aiValues[i]), + sYUnit, sPlusMinus, + oSeries.aiRevisions[i], + ); + if oRevInfo.sAuthor is not None: + sMsg = oRevInfo.sMessage[:80].strip(); + #if sMsg.find('\n') >= 0: + # sMsg = sMsg[:sMsg.find('\n')].strip(); + sTooltip += '' \ + '' \ + '' \ + % ( oRevInfo.sAuthor, + self.formatTsShort(oRevInfo.tsCreated), + sMsg, '...' if len(oRevInfo.sMessage) > len(sMsg) else ''); + sTooltip += '
%s:%s %s %s
Rev:r%s
Author:%s
Date:%s
Message:%s%s
'; + asHtmlTooltips.append(sTooltip); + oData.addDataSeries(sSeriesName, oSeries.aiRevisions, oSeries.aiValues, asHtmlTooltips, + oSeries.aiErrorBarBelow, oSeries.aiErrorBarAbove); + # Render the data into a graph. + oGraph = self.oGraphClass('tmgraph-%u' % (iGraph,), oData, self._oDisp); + self._configureGraph(oGraph); + + oGraph.setTitle(self._calcGraphName(aoSeries[0], fSeriesName, sSampleName)); + sHtml += '
\n' % (iGraph,); + sHtml += oGraph.renderGraph(); + sHtml += '\n
\n'; + iGraph += 1; + + # + # Emit raw tabular data if requested. + # + if self._dParams[WuiMain.ksParamGraphWizTabular]: + sHtml += '
\n' \ + ' \n' \ + % (iCollection, ); + for aoSeries in aaoSeries: + if aoSeries[0].iUnit < len(constants.valueunit.g_asNames) and aoSeries[0].iUnit > 0: + sUnit = constants.valueunit.g_asNames[aoSeries[0].iUnit]; + else: + sUnit = str(aoSeries[0].iUnit); + + for iSeries, oSeries in enumerate(aoSeries): + sColor = self.oGraphClass.calcSeriesColor(iSeries); + + sHtml += '\n' \ + ' \n' \ + ' \n' \ + ' \n' \ + ' \n' \ + ' ' \ + '\n' \ + ' \n' \ + '\n' \ + % ( sColor, + self._getSeriesNameFromBits(oSeries, self.kfSeriesName_All & ~self.kfSeriesName_OsArchs), + sUnit ); + + for i, iRevision in enumerate(oSeries.aiRevisions): + sHtml += ' \n' \ + % ( 'tmodd' if i & 1 else 'tmeven', + iRevision, oSeries.aiValues[i], + oSeries.aiErrorBarAbove[i], oSeries.aiErrorBarBelow[i], + oSeries.acSamples[i]); + sHtml += '
   %s
RevisionValue (%s)ΔmaxΔminSamples
r%s%s+%s-%s%s
\n' \ + '
\n'; + else: + sHtml += 'No results.\n'; + sHtml += '
\n' + sHtml += '
\n'; + + # + # Finish the form. + # + if fInteractive: + sHtml += sEndOfForm; + + return sHtml; + -- cgit v1.2.3