[52776] | 1 | # -*- coding: utf-8 -*-
|
---|
| 2 | # $Id: reporting.py 98103 2023-01-17 14:15:46Z vboxsync $
|
---|
| 3 |
|
---|
| 4 | """
|
---|
| 5 | Test Result Report Writer.
|
---|
| 6 |
|
---|
| 7 | This takes a processed test result tree and creates a HTML, re-structured text,
|
---|
| 8 | or normal text report from it.
|
---|
| 9 | """
|
---|
| 10 |
|
---|
| 11 | __copyright__ = \
|
---|
| 12 | """
|
---|
[98103] | 13 | Copyright (C) 2010-2023 Oracle and/or its affiliates.
|
---|
[52776] | 14 |
|
---|
[96407] | 15 | This file is part of VirtualBox base platform packages, as
|
---|
| 16 | available from https://www.virtualbox.org.
|
---|
[52776] | 17 |
|
---|
[96407] | 18 | This program is free software; you can redistribute it and/or
|
---|
| 19 | modify it under the terms of the GNU General Public License
|
---|
| 20 | as published by the Free Software Foundation, in version 3 of the
|
---|
| 21 | License.
|
---|
| 22 |
|
---|
| 23 | This program is distributed in the hope that it will be useful, but
|
---|
| 24 | WITHOUT ANY WARRANTY; without even the implied warranty of
|
---|
| 25 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
---|
| 26 | General Public License for more details.
|
---|
| 27 |
|
---|
| 28 | You should have received a copy of the GNU General Public License
|
---|
| 29 | along with this program; if not, see <https://www.gnu.org/licenses>.
|
---|
| 30 |
|
---|
[52776] | 31 | The contents of this file may alternatively be used under the terms
|
---|
| 32 | of the Common Development and Distribution License Version 1.0
|
---|
[96407] | 33 | (CDDL), a copy of it is provided in the "COPYING.CDDL" file included
|
---|
| 34 | in the VirtualBox distribution, in which case the provisions of the
|
---|
[52776] | 35 | CDDL are applicable instead of those of the GPL.
|
---|
| 36 |
|
---|
| 37 | You may elect to license modified versions of this file under the
|
---|
| 38 | terms and conditions of either the GPL or the CDDL or both.
|
---|
[96407] | 39 |
|
---|
| 40 | SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
|
---|
[52776] | 41 | """
|
---|
[97266] | 42 |
|
---|
[52776] | 43 | __version__ = "$Revision: 98103 $"
|
---|
| 44 |
|
---|
[97266] | 45 | # Standard python imports.
|
---|
| 46 | import os;
|
---|
| 47 | import sys;
|
---|
[52776] | 48 |
|
---|
[97266] | 49 | # Only the main script needs to modify the path.
|
---|
| 50 | try: __file__;
|
---|
| 51 | except: __file__ = sys.argv[0];
|
---|
| 52 | g_ksValidationKitDir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)));
|
---|
| 53 | sys.path.append(g_ksValidationKitDir);
|
---|
| 54 |
|
---|
| 55 | # ValidationKit imports.
|
---|
| 56 | from common import utils;
|
---|
| 57 |
|
---|
| 58 | # Python 3 hacks:
|
---|
| 59 | if sys.version_info[0] >= 3:
|
---|
| 60 | long = int; # pylint: disable=redefined-builtin,invalid-name
|
---|
| 61 |
|
---|
| 62 |
|
---|
| 63 | ##################################################################################################################################
|
---|
| 64 | # Run Table #
|
---|
| 65 | ##################################################################################################################################
|
---|
| 66 |
|
---|
| 67 | def alignTextLeft(sText, cchWidth):
|
---|
| 68 | """ Left aligns text and pads it to cchWidth characters length. """
|
---|
| 69 | return sText + ' ' * (cchWidth - min(len(sText), cchWidth));
|
---|
| 70 |
|
---|
| 71 |
|
---|
| 72 | def alignTextRight(sText, cchWidth):
|
---|
| 73 | """ Right aligns text and pads it to cchWidth characters length. """
|
---|
| 74 | return ' ' * (cchWidth - min(len(sText), cchWidth)) + sText;
|
---|
| 75 |
|
---|
| 76 |
|
---|
| 77 | def alignTextCenter(sText, cchWidth):
|
---|
| 78 | """ Pads the text equally on both sides to cchWidth characters length. """
|
---|
| 79 | return alignTextLeft(' ' * ((cchWidth - min(len(sText), cchWidth)) // 2) + sText, cchWidth);
|
---|
| 80 |
|
---|
| 81 |
|
---|
| 82 | g_kiAlignLeft = -1;
|
---|
| 83 | g_kiAlignRight = 1;
|
---|
| 84 | g_kiAlignCenter = 0;
|
---|
| 85 | def alignText(sText, cchWidth, iAlignType):
|
---|
| 86 | """
|
---|
| 87 | General alignment method.
|
---|
| 88 |
|
---|
| 89 | Negative iAlignType for left aligning, zero for entered, and positive for
|
---|
| 90 | right aligning the text.
|
---|
| 91 | """
|
---|
| 92 | if iAlignType < 0:
|
---|
| 93 | return alignTextLeft(sText, cchWidth);
|
---|
| 94 | if iAlignType > 0:
|
---|
| 95 | return alignTextRight(sText, cchWidth);
|
---|
| 96 | return alignTextCenter(sText, cchWidth);
|
---|
| 97 |
|
---|
| 98 |
|
---|
| 99 | class TextColumnWidth(object):
|
---|
| 100 | """
|
---|
| 101 | Tracking the width of a column, dealing with sub-columns and such.
|
---|
| 102 | """
|
---|
| 103 |
|
---|
| 104 | def __init__(self):
|
---|
| 105 | self.cch = 0;
|
---|
| 106 | self.dacchSub = {};
|
---|
| 107 |
|
---|
| 108 | def update(self, oWidth, cchSubColSpacing = 1):
|
---|
| 109 | """
|
---|
| 110 | Updates the column width tracking with oWidth, which is either
|
---|
| 111 | an int or an array of ints (sub columns).
|
---|
| 112 | """
|
---|
| 113 | if isinstance(oWidth, int):
|
---|
| 114 | self.cch = max(self.cch, oWidth);
|
---|
| 115 | else:
|
---|
| 116 | cSubCols = len(oWidth);
|
---|
| 117 | if cSubCols not in self.dacchSub:
|
---|
| 118 | self.dacchSub[cSubCols] = list(oWidth);
|
---|
| 119 | self.cch = max(self.cch, sum(oWidth) + cchSubColSpacing * (cSubCols - 1));
|
---|
| 120 | else:
|
---|
| 121 | acchSubCols = self.dacchSub[cSubCols];
|
---|
| 122 | for iSub in range(cSubCols):
|
---|
| 123 | acchSubCols[iSub] = max(acchSubCols[iSub], oWidth[iSub]);
|
---|
| 124 | self.cch = max(self.cch, sum(acchSubCols) + cchSubColSpacing * (cSubCols - 1));
|
---|
| 125 |
|
---|
| 126 | def finalize(self):
|
---|
| 127 | """ Finalizes sub-column sizes. """
|
---|
| 128 | ## @todo maybe do something here, maybe not...
|
---|
| 129 | return self;
|
---|
| 130 |
|
---|
| 131 | def hasSubColumns(self):
|
---|
| 132 | """ Checks if there are sub-columns for this column. """
|
---|
| 133 | return not self.dacchSub;
|
---|
| 134 |
|
---|
| 135 | class TextWidths(object):
|
---|
| 136 | """
|
---|
| 137 | Tracks the column widths for text rending of the table.
|
---|
| 138 | """
|
---|
| 139 | def __init__(self, cchSubColSpacing = 1, ):
|
---|
| 140 | self.cchName = 1;
|
---|
| 141 | self.aoColumns = [] # type: TextColumnWidth
|
---|
| 142 | self.cchSubColSpacing = cchSubColSpacing;
|
---|
| 143 | self.fFinalized = False;
|
---|
| 144 |
|
---|
| 145 | def update(self, aoWidths):
|
---|
| 146 | """ Updates the tracker with the returns of calcColumnWidthsForText. """
|
---|
| 147 | if not aoWidths[0]:
|
---|
| 148 | self.cchName = max(self.cchName, aoWidths[1]);
|
---|
| 149 |
|
---|
| 150 | for iCol, oWidth in enumerate(aoWidths[2]):
|
---|
| 151 | if iCol >= len(self.aoColumns):
|
---|
| 152 | self.aoColumns.append(TextColumnWidth());
|
---|
| 153 | self.aoColumns[iCol].update(oWidth, self.cchSubColSpacing);
|
---|
| 154 |
|
---|
| 155 | return self;
|
---|
| 156 |
|
---|
| 157 | def finalize(self):
|
---|
| 158 | """ Finalizes sub-column sizes. """
|
---|
| 159 | for oColumnWidth in self.aoColumns:
|
---|
| 160 | oColumnWidth.finalize();
|
---|
| 161 | self.fFinalized = True;
|
---|
| 162 | return self;
|
---|
| 163 |
|
---|
| 164 | def getColumnWidth(self, iColumn, cSubs = None, iSub = None):
|
---|
| 165 | """ Returns the width of the specified column. """
|
---|
| 166 | if not self.fFinalized:
|
---|
| 167 | return 0;
|
---|
| 168 | assert iColumn < len(self.aoColumns), "iColumn=%s vs %s" % (iColumn, len(self.aoColumns),);
|
---|
| 169 | oColumn = self.aoColumns[iColumn];
|
---|
| 170 | if cSubs is not None:
|
---|
| 171 | assert iSub < cSubs;
|
---|
| 172 | if cSubs != 1:
|
---|
| 173 | assert cSubs in oColumn.dacchSub, \
|
---|
| 174 | "iColumn=%s cSubs=%s iSub=%s; dacchSub=%s" % (iColumn, cSubs, iSub, oColumn.dacchSub);
|
---|
| 175 | return oColumn.dacchSub[cSubs][iSub];
|
---|
| 176 | return oColumn.cch;
|
---|
| 177 |
|
---|
| 178 |
|
---|
| 179 | class TextElement(object):
|
---|
| 180 | """
|
---|
| 181 | A text element (cell/sub-cell in a table).
|
---|
| 182 | """
|
---|
| 183 |
|
---|
| 184 | def __init__(self, sText = '', iAlign = g_kiAlignRight): # type: (str, int) -> None
|
---|
| 185 | self.sText = sText;
|
---|
| 186 | self.iAlign = iAlign;
|
---|
| 187 |
|
---|
[97279] | 188 | def asText(self, cchWidth): # type: (int) -> str
|
---|
| 189 | """ Pads the text to width of cchWidth characters. """
|
---|
| 190 | return alignText(self.sText, cchWidth, self.iAlign);
|
---|
[97266] | 191 |
|
---|
[97279] | 192 |
|
---|
[97266] | 193 | class RunRow(object):
|
---|
| 194 | """
|
---|
| 195 | Run table row.
|
---|
| 196 | """
|
---|
| 197 |
|
---|
| 198 | def __init__(self, iLevel, sName, iRun = 0): # type: (int, str, int) -> None
|
---|
| 199 | self.iLevel = iLevel;
|
---|
| 200 | self.sName = sName;
|
---|
| 201 | self.iFirstRun = iRun;
|
---|
| 202 |
|
---|
| 203 | # Fields used while formatting (set during construction or calcColumnWidthsForText/Html).
|
---|
| 204 | self.cColumns = 0; ##< Number of columns.
|
---|
| 205 | self.fSkip = False ##< Whether or not to skip this row in the output.
|
---|
| 206 |
|
---|
| 207 | # Format as Text:
|
---|
| 208 |
|
---|
[97279] | 209 | def formatNameAsText(self, cchWidth): # (int) -> TextElement
|
---|
[97266] | 210 | """ Format the row as text. """
|
---|
| 211 | _ = cchWidth;
|
---|
[97279] | 212 | return TextElement(' ' * (self.iLevel * 2) + self.sName, g_kiAlignLeft);
|
---|
[97266] | 213 |
|
---|
| 214 | def getColumnCountAsText(self, oTable):
|
---|
| 215 | """
|
---|
| 216 | Called by calcColumnWidthsForText for getting an up-to-date self.cColumns value.
|
---|
| 217 | Override this to update cColumns after construction.
|
---|
| 218 | """
|
---|
| 219 | _ = oTable;
|
---|
| 220 | return self.cColumns;
|
---|
| 221 |
|
---|
| 222 | def formatColumnAsText(self, iColumn, oTable): # type: (int, RunTable) -> [TextElement]
|
---|
| 223 | """ Returns an array of TextElements for the given column in this row. """
|
---|
| 224 | _ = iColumn; _ = oTable;
|
---|
| 225 | return [ TextElement(),];
|
---|
| 226 |
|
---|
[97279] | 227 | def calcColumnWidthsForText(self, oTable): # type: (RunTable) -> (bool, int, [])
|
---|
[97266] | 228 | """
|
---|
| 229 | Calculates the column widths for text rendering.
|
---|
| 230 |
|
---|
| 231 | Returns a tuple consisting of the fSkip, the formatted name width, and an
|
---|
| 232 | array of column widths. The entries in the latter are either integer
|
---|
| 233 | widths or arrays of subcolumn integer widths.
|
---|
| 234 | """
|
---|
| 235 | aoRetCols = [];
|
---|
| 236 | cColumns = self.getColumnCountAsText(oTable);
|
---|
| 237 | for iColumn in range(cColumns):
|
---|
| 238 | aoSubColumns = self.formatColumnAsText(iColumn, oTable);
|
---|
| 239 | if len(aoSubColumns) == 1:
|
---|
| 240 | aoRetCols.append(len(aoSubColumns[0].sText));
|
---|
| 241 | else:
|
---|
| 242 | aoRetCols.append([len(oSubColumn.sText) for oSubColumn in aoSubColumns]);
|
---|
[97279] | 243 | return (False, len(self.formatNameAsText(0).sText), aoRetCols);
|
---|
[97266] | 244 |
|
---|
[97279] | 245 | def renderAsText(self, oWidths, oTable): # type: (TextWidths, RunTable) -> str
|
---|
| 246 | """
|
---|
| 247 | Renders the row as text.
|
---|
| 248 |
|
---|
| 249 | Returns string.
|
---|
| 250 | """
|
---|
| 251 | sRow = self.formatNameAsText(oWidths.cchName).asText(oWidths.cchName);
|
---|
| 252 | sRow = sRow + ' ' * (oWidths.cchName - min(len(sRow), oWidths.cchName)) + ' : ';
|
---|
| 253 |
|
---|
| 254 | for iColumn in range(self.cColumns):
|
---|
| 255 | aoSubCols = self.formatColumnAsText(iColumn, oTable);
|
---|
| 256 | sCell = '';
|
---|
| 257 | for iSub, oText in enumerate(aoSubCols):
|
---|
| 258 | cchWidth = oWidths.getColumnWidth(iColumn, len(aoSubCols), iSub);
|
---|
| 259 | if iSub > 0:
|
---|
| 260 | sCell += ' ' * oWidths.cchSubColSpacing;
|
---|
| 261 | sCell += oText.asText(cchWidth);
|
---|
| 262 | cchWidth = oWidths.getColumnWidth(iColumn);
|
---|
| 263 | sRow += (' | ' if iColumn > 0 else '') + ' ' * (cchWidth - min(cchWidth, len(sCell))) + sCell;
|
---|
| 264 |
|
---|
| 265 | return sRow;
|
---|
| 266 |
|
---|
[97266] | 267 | @staticmethod
|
---|
| 268 | def formatDiffAsText(lNumber, lBaseline):
|
---|
| 269 | """ Formats the difference between lNumber and lBaseline as text. """
|
---|
| 270 | if lNumber is not None:
|
---|
| 271 | if lBaseline is not None:
|
---|
| 272 | if lNumber < lBaseline:
|
---|
| 273 | return '-' + utils.formatNumber(lBaseline - lNumber); ## @todo formatter is busted for negative nums.
|
---|
| 274 | if lNumber > lBaseline:
|
---|
| 275 | return '+' + utils.formatNumber(lNumber - lBaseline);
|
---|
| 276 | return '0';
|
---|
| 277 | return '';
|
---|
| 278 |
|
---|
| 279 | @staticmethod
|
---|
[97279] | 280 | def formatPctAsText(chSign, rdPct, cPctPrecision):
|
---|
| 281 | """ Formats percentage value as text. """
|
---|
| 282 | if rdPct >= 100:
|
---|
| 283 | return '%s%s%%' % (chSign, utils.formatNumber(int(rdPct + 0.5)),);
|
---|
[97280] | 284 | if round(rdPct, cPctPrecision) != 0:
|
---|
| 285 | return '%s%.*f%%' % (chSign, cPctPrecision, rdPct,); # %.*f rounds.
|
---|
[97279] | 286 | return '~' + chSign + '0.' + '0' * cPctPrecision + '%';
|
---|
| 287 |
|
---|
| 288 | @staticmethod
|
---|
[97266] | 289 | def formatDiffInPctAsText(lNumber, lBaseline, cPctPrecision):
|
---|
| 290 | """ Formats the difference between lNumber and lBaseline in precent as text. """
|
---|
| 291 | if lNumber is not None:
|
---|
| 292 | if lBaseline is not None:
|
---|
| 293 | ## @todo implement cPctPrecision
|
---|
| 294 | if lNumber == lBaseline:
|
---|
| 295 | return '0.' + '0'*cPctPrecision + '%';
|
---|
| 296 |
|
---|
| 297 | lDiff = lNumber - lBaseline;
|
---|
| 298 | chSign = '+';
|
---|
| 299 | if lDiff < 0:
|
---|
| 300 | lDiff = -lDiff;
|
---|
| 301 | chSign = '-';
|
---|
[97279] | 302 | return RunRow.formatPctAsText(chSign, lDiff / float(lBaseline) * 100, cPctPrecision);
|
---|
| 303 | return '';
|
---|
[97266] | 304 |
|
---|
| 305 |
|
---|
[97279] | 306 | class RunHeaderRow(RunRow):
|
---|
| 307 | """
|
---|
| 308 | Run table header row.
|
---|
| 309 | """
|
---|
| 310 | def __init__(self, sName, asColumns): # type: (str, [str]) -> None
|
---|
| 311 | RunRow.__init__(self, 0, sName);
|
---|
| 312 | self.asColumns = asColumns
|
---|
| 313 | self.cColumns = len(asColumns);
|
---|
[97266] | 314 |
|
---|
[97279] | 315 | def formatColumnAsText(self, iColumn, oTable): # type: (int, RunTable) -> [TextElement]
|
---|
| 316 | return [TextElement(self.asColumns[iColumn], g_kiAlignCenter),];
|
---|
[97266] | 317 |
|
---|
| 318 |
|
---|
[97279] | 319 | class RunFooterRow(RunHeaderRow):
|
---|
| 320 | """
|
---|
| 321 | Run table footer row.
|
---|
| 322 | """
|
---|
| 323 | def __init__(self, sName, asColumns):
|
---|
| 324 | RunHeaderRow.__init__(self, sName, asColumns);
|
---|
| 325 |
|
---|
| 326 |
|
---|
| 327 | class RunSeparatorRow(RunRow):
|
---|
| 328 | """
|
---|
| 329 | Base class for separator rows.
|
---|
| 330 | """
|
---|
| 331 | def __init__(self):
|
---|
| 332 | RunRow.__init__(self, 0, '');
|
---|
| 333 |
|
---|
| 334 | def calcTableWidthAsText(self, oWidths):
|
---|
| 335 | """ Returns the table width for when rendered as text. """
|
---|
| 336 | cchWidth = oWidths.cchName;
|
---|
| 337 | for oCol in oWidths.aoColumns:
|
---|
| 338 | cchWidth += 3 + oCol.cch;
|
---|
| 339 | return cchWidth;
|
---|
| 340 |
|
---|
| 341 |
|
---|
| 342 | class RunHeaderSeparatorRow(RunSeparatorRow):
|
---|
| 343 | """
|
---|
| 344 | Run table header separator row.
|
---|
| 345 | """
|
---|
| 346 | def __init__(self):
|
---|
| 347 | RunSeparatorRow.__init__(self);
|
---|
| 348 |
|
---|
| 349 | def renderAsText(self, oWidths, oTable):
|
---|
| 350 | _ = oTable;
|
---|
| 351 | return '=' * self.calcTableWidthAsText(oWidths);
|
---|
| 352 |
|
---|
| 353 |
|
---|
| 354 | class RunFooterSeparatorRow(RunHeaderSeparatorRow):
|
---|
| 355 | """
|
---|
| 356 | Run table footer separator row.
|
---|
| 357 | """
|
---|
| 358 | def __init__(self):
|
---|
| 359 | RunHeaderSeparatorRow.__init__(self);
|
---|
| 360 |
|
---|
| 361 |
|
---|
[97266] | 362 | class RunTestRow(RunRow):
|
---|
| 363 | """
|
---|
| 364 | Run table test row.
|
---|
| 365 | """
|
---|
| 366 |
|
---|
| 367 | def __init__(self, iLevel, oTest, iRun, aoTests = None): # type: (int, reader.Test, int, [reader.Test]) -> None
|
---|
| 368 | RunRow.__init__(self, iLevel, oTest.sName, iRun);
|
---|
| 369 | assert oTest;
|
---|
| 370 | self.oTest = oTest;
|
---|
| 371 | if aoTests is None:
|
---|
| 372 | aoTests = [None for i in range(iRun)];
|
---|
| 373 | aoTests.append(oTest);
|
---|
| 374 | else:
|
---|
| 375 | aoTests= list(aoTests);
|
---|
| 376 | self.aoTests = aoTests
|
---|
| 377 |
|
---|
| 378 | def isSameTest(self, oTest):
|
---|
| 379 | """ Checks if oTest belongs to this row or not. """
|
---|
| 380 | return oTest.sName == self.oTest.sName;
|
---|
| 381 |
|
---|
| 382 | def getBaseTest(self, oTable):
|
---|
| 383 | """ Returns the baseline test. """
|
---|
| 384 | oBaseTest = self.aoTests[oTable.iBaseline];
|
---|
| 385 | if not oBaseTest:
|
---|
| 386 | oBaseTest = self.aoTests[self.iFirstRun];
|
---|
| 387 | return oBaseTest;
|
---|
| 388 |
|
---|
| 389 |
|
---|
| 390 | class RunTestStartRow(RunTestRow):
|
---|
| 391 | """
|
---|
| 392 | Run table start of test row.
|
---|
| 393 | """
|
---|
| 394 |
|
---|
| 395 | def __init__(self, iLevel, oTest, iRun): # type: (int, reader.Test, int) -> None
|
---|
| 396 | RunTestRow.__init__(self, iLevel, oTest, iRun);
|
---|
| 397 |
|
---|
[97279] | 398 | def renderAsText(self, oWidths, oTable):
|
---|
| 399 | _ = oTable;
|
---|
| 400 | sRet = self.formatNameAsText(oWidths.cchName).asText(oWidths.cchName);
|
---|
| 401 | sRet += ' : ';
|
---|
| 402 | sRet += ' | '.join(['-' * oCol.cch for oCol in oWidths.aoColumns]);
|
---|
| 403 | return sRet;
|
---|
| 404 |
|
---|
| 405 |
|
---|
[97266] | 406 | class RunTestEndRow(RunTestRow):
|
---|
| 407 | """
|
---|
| 408 | Run table end of test row.
|
---|
| 409 | """
|
---|
| 410 |
|
---|
| 411 | def __init__(self, oStartRow): # type: (RunTestStartRow) -> None
|
---|
| 412 | RunTestRow.__init__(self, oStartRow.iLevel, oStartRow.oTest, oStartRow.iFirstRun, oStartRow.aoTests);
|
---|
| 413 | self.oStartRow = oStartRow # type: RunTestStartRow
|
---|
| 414 |
|
---|
| 415 | def getColumnCountAsText(self, oTable):
|
---|
| 416 | self.cColumns = len(self.aoTests);
|
---|
| 417 | return self.cColumns;
|
---|
| 418 |
|
---|
| 419 | def formatColumnAsText(self, iColumn, oTable):
|
---|
| 420 | oTest = self.aoTests[iColumn];
|
---|
| 421 | if oTest and oTest.sStatus:
|
---|
| 422 | if oTest.cErrors > 0:
|
---|
| 423 | return [ TextElement(oTest.sStatus, g_kiAlignCenter),
|
---|
| 424 | TextElement(utils.formatNumber(oTest.cErrors) + 'errors') ];
|
---|
| 425 | return [ TextElement(oTest.sStatus, g_kiAlignCenter) ];
|
---|
| 426 | return [ TextElement(), ];
|
---|
| 427 |
|
---|
| 428 |
|
---|
| 429 | class RunTestEndRow2(RunTestRow):
|
---|
| 430 | """
|
---|
| 431 | Run table 2nd end of test row, this shows the times.
|
---|
| 432 | """
|
---|
| 433 |
|
---|
| 434 | def __init__(self, oStartRow): # type: (RunTestStartRow) -> None
|
---|
| 435 | RunTestRow.__init__(self, oStartRow.iLevel, oStartRow.oTest, oStartRow.iFirstRun, oStartRow.aoTests);
|
---|
| 436 | self.oStartRow = oStartRow # type: RunTestStartRow
|
---|
| 437 |
|
---|
| 438 | def formatNameAsText(self, cchWidth):
|
---|
| 439 | _ = cchWidth;
|
---|
[97279] | 440 | return TextElement('runtime', g_kiAlignRight);
|
---|
[97266] | 441 |
|
---|
| 442 | def getColumnCountAsText(self, oTable):
|
---|
| 443 | self.cColumns = len(self.aoTests);
|
---|
| 444 | return self.cColumns;
|
---|
| 445 |
|
---|
| 446 | def formatColumnAsText(self, iColumn, oTable):
|
---|
| 447 | oTest = self.aoTests[iColumn];
|
---|
| 448 | if oTest:
|
---|
| 449 | cUsElapsed = oTest.calcDurationAsMicroseconds();
|
---|
| 450 | if cUsElapsed:
|
---|
| 451 | oBaseTest = self.getBaseTest(oTable);
|
---|
| 452 | if oTest is oBaseTest:
|
---|
| 453 | return [ TextElement(utils.formatNumber(cUsElapsed)), TextElement('us', g_kiAlignLeft), ];
|
---|
| 454 | cUsElapsedBase = oBaseTest.calcDurationAsMicroseconds();
|
---|
| 455 | aoRet = [
|
---|
| 456 | TextElement(utils.formatNumber(cUsElapsed)),
|
---|
| 457 | TextElement(self.formatDiffAsText(cUsElapsed, cUsElapsedBase)),
|
---|
| 458 | TextElement(self.formatDiffInPctAsText(cUsElapsed, cUsElapsedBase, oTable.cPctPrecision)),
|
---|
| 459 | ];
|
---|
| 460 | return aoRet[1:] if oTable.fBrief else aoRet;
|
---|
| 461 | return [ TextElement(), ];
|
---|
| 462 |
|
---|
[97279] | 463 |
|
---|
| 464 | class RunTestValueAnalysisRow(RunTestRow):
|
---|
| 465 | """
|
---|
| 466 | Run table row with value analysis for a test, see if we have an improvement or not.
|
---|
| 467 | """
|
---|
| 468 | def __init__(self, oStartRow): # type: (RunTestStartRow) -> None
|
---|
| 469 | RunTestRow.__init__(self, oStartRow.iLevel, oStartRow.oTest, oStartRow.iFirstRun, oStartRow.aoTests);
|
---|
| 470 | self.oStartRow = oStartRow # type: RunTestStartRow
|
---|
| 471 | self.cColumns = len(self.aoTests);
|
---|
| 472 |
|
---|
| 473 | def formatNameAsText(self, cchWidth):
|
---|
| 474 | _ = cchWidth;
|
---|
| 475 | return TextElement('value analysis', g_kiAlignRight);
|
---|
| 476 |
|
---|
| 477 | def formatColumnAsText(self, iColumn, oTable):
|
---|
| 478 | oBaseline = self.getBaseTest(oTable);
|
---|
| 479 | oTest = self.aoTests[iColumn];
|
---|
| 480 | if not oTest or oTest is oBaseline:
|
---|
| 481 | return [TextElement(),];
|
---|
| 482 |
|
---|
| 483 | #
|
---|
| 484 | # This is a bit ugly, but it means we don't have to re-merge the values.
|
---|
| 485 | #
|
---|
| 486 | cTotal = 0;
|
---|
| 487 | cBetter = 0;
|
---|
| 488 | cWorse = 0;
|
---|
| 489 | cSame = 0;
|
---|
| 490 | cUncertain = 0;
|
---|
| 491 | rdPctTotal = 0.0;
|
---|
| 492 |
|
---|
| 493 | iRow = oTable.aoRows.index(self.oStartRow); # ugly
|
---|
| 494 | while iRow < len(oTable.aoRows):
|
---|
| 495 | oRow = oTable.aoRows[iRow];
|
---|
| 496 | if oRow is self:
|
---|
| 497 | break;
|
---|
| 498 | if isinstance(oRow, RunValueRow):
|
---|
| 499 | oValue = oRow.aoValues[iColumn];
|
---|
| 500 | oBaseValue = oRow.getBaseValue(oTable);
|
---|
| 501 | if oValue is not None and oValue is not oBaseValue:
|
---|
| 502 | iBetter = oValue.getBetterRelation();
|
---|
| 503 | if iBetter != 0:
|
---|
| 504 | lDiff = oValue.lValue - oBaseValue.lValue;
|
---|
| 505 | rdPct = abs(lDiff / float(oBaseValue.lValue) * 100);
|
---|
| 506 | if rdPct < oTable.rdPctSameValue:
|
---|
| 507 | cSame += 1;
|
---|
| 508 | else:
|
---|
| 509 | if lDiff > 0 if iBetter > 0 else lDiff < 0:
|
---|
| 510 | cBetter += 1;
|
---|
| 511 | rdPctTotal += rdPct;
|
---|
| 512 | else:
|
---|
| 513 | cWorse += 1;
|
---|
| 514 | rdPctTotal += -rdPct;
|
---|
| 515 | cUncertain += 1 if iBetter in (1, -1) else 0;
|
---|
| 516 | cTotal += 1;
|
---|
| 517 | iRow += 1;
|
---|
| 518 |
|
---|
| 519 | #
|
---|
| 520 | # Format the result.
|
---|
| 521 | #
|
---|
| 522 | aoRet = [];
|
---|
| 523 | if not oTable.fBrief:
|
---|
| 524 | sText = u' \u2193%u' % (cWorse,);
|
---|
| 525 | sText = u' \u2248%u' % (cSame,) + alignTextRight(sText, 4);
|
---|
| 526 | sText = u'\u2191%u' % (cBetter,) + alignTextRight(sText, 8);
|
---|
| 527 | aoRet = [TextElement(sText),];
|
---|
| 528 |
|
---|
| 529 | if cSame >= cWorse and cSame >= cBetter:
|
---|
| 530 | sVerdict = 'same';
|
---|
| 531 | elif cWorse >= cSame and cWorse >= cBetter:
|
---|
| 532 | sVerdict = 'worse';
|
---|
| 533 | else:
|
---|
| 534 | sVerdict = 'better';
|
---|
| 535 | if cUncertain > 0:
|
---|
| 536 | sVerdict = 'probably ' + sVerdict;
|
---|
| 537 | aoRet.append(TextElement(sVerdict));
|
---|
| 538 |
|
---|
| 539 | rdPctAvg = abs(rdPctTotal / cTotal); # Yes, average of the percentages!
|
---|
| 540 | aoRet.append(TextElement(self.formatPctAsText('+' if rdPctTotal >= 0 else '-', rdPctAvg, oTable.cPctPrecision)));
|
---|
| 541 |
|
---|
| 542 | return aoRet;
|
---|
| 543 |
|
---|
| 544 |
|
---|
[97266] | 545 | class RunValueRow(RunRow):
|
---|
| 546 | """
|
---|
| 547 | Run table value row.
|
---|
| 548 | """
|
---|
| 549 |
|
---|
| 550 | def __init__(self, iLevel, oValue, iRun): # type: (int, reader.Value, int) -> None
|
---|
| 551 | RunRow.__init__(self, iLevel, oValue.sName, iRun);
|
---|
| 552 | self.oValue = oValue;
|
---|
| 553 | self.aoValues = [None for i in range(iRun)];
|
---|
| 554 | self.aoValues.append(oValue);
|
---|
| 555 |
|
---|
| 556 | def isSameValue(self, oValue):
|
---|
| 557 | """ Checks if oValue belongs to this row or not. """
|
---|
| 558 | return oValue.sName == self.oValue.sName and oValue.sUnit == self.oValue.sUnit;
|
---|
| 559 |
|
---|
| 560 | # Formatting as Text.
|
---|
| 561 |
|
---|
| 562 | @staticmethod
|
---|
| 563 | def formatOneValueAsText(oValue): # type: (reader.Value) -> str
|
---|
| 564 | """ Formats a value. """
|
---|
| 565 | if not oValue:
|
---|
| 566 | return "N/A";
|
---|
| 567 | return utils.formatNumber(oValue.lValue);
|
---|
| 568 |
|
---|
| 569 | def getBaseValue(self, oTable):
|
---|
| 570 | """ Returns the base value instance. """
|
---|
| 571 | oBaseValue = self.aoValues[oTable.iBaseline];
|
---|
| 572 | if not oBaseValue:
|
---|
| 573 | oBaseValue = self.aoValues[self.iFirstRun];
|
---|
| 574 | return oBaseValue;
|
---|
| 575 |
|
---|
| 576 | def getColumnCountAsText(self, oTable):
|
---|
| 577 | self.cColumns = len(self.aoValues);
|
---|
| 578 | return self.cColumns;
|
---|
| 579 |
|
---|
| 580 | def formatColumnAsText(self, iColumn, oTable):
|
---|
| 581 | oValue = self.aoValues[iColumn];
|
---|
| 582 | oBaseValue = self.getBaseValue(oTable);
|
---|
| 583 | if oValue is oBaseValue:
|
---|
| 584 | return [ TextElement(self.formatOneValueAsText(oValue)),
|
---|
| 585 | TextElement(oValue.sUnit, g_kiAlignLeft), ];
|
---|
| 586 | aoRet = [
|
---|
| 587 | TextElement(self.formatOneValueAsText(oValue)),
|
---|
| 588 | TextElement(self.formatDiffAsText(oValue.lValue if oValue else None, oBaseValue.lValue)),
|
---|
| 589 | TextElement(self.formatDiffInPctAsText(oValue.lValue if oValue else None, oBaseValue.lValue, oTable.cPctPrecision))
|
---|
| 590 | ];
|
---|
| 591 | return aoRet[1:] if oTable.fBrief else aoRet;
|
---|
| 592 |
|
---|
| 593 |
|
---|
| 594 | class RunTable(object):
|
---|
| 595 | """
|
---|
| 596 | Result table.
|
---|
| 597 |
|
---|
| 598 | This contains one or more test runs as columns.
|
---|
| 599 | """
|
---|
| 600 |
|
---|
[97279] | 601 | def __init__(self, iBaseline = 0, fBrief = True, cPctPrecision = 2, rdPctSameValue = 0.10): # (int, bool, int, float) -> None
|
---|
| 602 | self.asColumns = [] # type: [str] ##< Column names.
|
---|
| 603 | self.aoRows = [] # type: [RunRow] ##< The table rows.
|
---|
| 604 | self.iBaseline = iBaseline # type: int ##< Which column is the baseline when diffing things.
|
---|
| 605 | self.fBrief = fBrief # type: bool ##< Whether to exclude the numerical values of non-baseline runs.
|
---|
| 606 | self.cPctPrecision = cPctPrecision # type: int ##< Number of decimal points in diff percentage value.
|
---|
| 607 | self.rdPctSameValue = rdPctSameValue # type: float ##< The percent value at which a value difference is considered
|
---|
| 608 | ## to be the same during value analysis.
|
---|
[97266] | 609 | def __populateFromValues(self, aaoValueRuns, iLevel): # type: ([reader.Value]) -> None
|
---|
| 610 | """
|
---|
| 611 | Internal worker for __populateFromRuns()
|
---|
| 612 |
|
---|
[97279] | 613 | This will modify the sub-lists inside aaoValueRuns, returning with them all empty.
|
---|
| 614 |
|
---|
| 615 | Returns True if an value analysis row should be added, False if not.
|
---|
[97266] | 616 | """
|
---|
| 617 | # Same as for __populateFromRuns, only no recursion.
|
---|
[97279] | 618 | fAnalysisRow = False;
|
---|
[97266] | 619 | for iValueRun, aoValuesForRun in enumerate(aaoValueRuns):
|
---|
| 620 | while aoValuesForRun:
|
---|
| 621 | oRow = RunValueRow(iLevel, aoValuesForRun.pop(0), iValueRun);
|
---|
| 622 | self.aoRows.append(oRow);
|
---|
| 623 |
|
---|
| 624 | # Pop matching values from the other runs of this test.
|
---|
| 625 | for iOtherRun in range(iValueRun + 1, len(aaoValueRuns)):
|
---|
| 626 | aoValuesForOtherRun = aaoValueRuns[iOtherRun];
|
---|
| 627 | for iValueToPop, oOtherValue in enumerate(aoValuesForOtherRun):
|
---|
| 628 | if oRow.isSameValue(oOtherValue):
|
---|
| 629 | oRow.aoValues.append(aoValuesForOtherRun.pop(iValueToPop));
|
---|
| 630 | break;
|
---|
| 631 | if len(oRow.aoValues) <= iOtherRun:
|
---|
| 632 | oRow.aoValues.append(None);
|
---|
| 633 |
|
---|
[97279] | 634 | fAnalysisRow = fAnalysisRow or oRow.oValue.canDoBetterCompare();
|
---|
| 635 | return fAnalysisRow;
|
---|
| 636 |
|
---|
[97266] | 637 | def __populateFromRuns(self, aaoTestRuns, iLevel): # type: ([reader.Test]) -> None
|
---|
| 638 | """
|
---|
| 639 | Internal worker for populateFromRuns()
|
---|
| 640 |
|
---|
[97279] | 641 | This will modify the sub-lists inside aaoTestRuns, returning with them all empty.
|
---|
[97266] | 642 | """
|
---|
| 643 |
|
---|
| 644 | #
|
---|
| 645 | # Currently doing depth first, so values are always at the end.
|
---|
| 646 | # Nominally, we should inject values according to the timestamp.
|
---|
| 647 | # However, that's too much work right now and can be done later if needed.
|
---|
| 648 | #
|
---|
| 649 | for iRun, aoTestForRun in enumerate(aaoTestRuns):
|
---|
| 650 | while aoTestForRun:
|
---|
| 651 | # Pop the next test and create a start-test row for it.
|
---|
| 652 | oStartRow = RunTestStartRow(iLevel, aoTestForRun.pop(0), iRun);
|
---|
| 653 | self.aoRows.append(oStartRow);
|
---|
| 654 |
|
---|
| 655 | # Pop matching tests from the other runs.
|
---|
| 656 | for iOtherRun in range(iRun + 1, len(aaoTestRuns)):
|
---|
| 657 | aoOtherTestRun = aaoTestRuns[iOtherRun];
|
---|
| 658 | for iTestToPop, oOtherTest in enumerate(aoOtherTestRun):
|
---|
| 659 | if oStartRow.isSameTest(oOtherTest):
|
---|
| 660 | oStartRow.aoTests.append(aoOtherTestRun.pop(iTestToPop));
|
---|
| 661 | break;
|
---|
| 662 | if len(oStartRow.aoTests) <= iOtherRun:
|
---|
| 663 | oStartRow.aoTests.append(None);
|
---|
| 664 |
|
---|
[97279] | 665 | # Now recursively do the subtests for it and then do the values.
|
---|
[97266] | 666 | self.__populateFromRuns( [list(oTest.aoChildren) if oTest else list() for oTest in oStartRow.aoTests], iLevel+1);
|
---|
[97279] | 667 | fValueAnalysisRow = self.__populateFromValues([list(oTest.aoValues)
|
---|
| 668 | if oTest else list() for oTest in oStartRow.aoTests], iLevel+1);
|
---|
[97266] | 669 |
|
---|
| 670 | # Add the end-test row for it.
|
---|
| 671 | self.aoRows.append(RunTestEndRow(oStartRow));
|
---|
| 672 | self.aoRows.append(RunTestEndRow2(oStartRow));
|
---|
[97279] | 673 | if fValueAnalysisRow:
|
---|
| 674 | self.aoRows.append(RunTestValueAnalysisRow(oStartRow));
|
---|
[97266] | 675 |
|
---|
| 676 | return self;
|
---|
| 677 |
|
---|
| 678 | def populateFromRuns(self, aoTestRuns, asRunNames = None): # type: ([reader.Test], [str]) -> RunTable
|
---|
| 679 | """
|
---|
| 680 | Populates the table from the series of runs.
|
---|
| 681 |
|
---|
| 682 | The aoTestRuns and asRunNames run in parallel. If the latter isn't
|
---|
| 683 | given, the names will just be ordinals starting with #0 for the
|
---|
| 684 | first column.
|
---|
| 685 |
|
---|
| 686 | Returns self.
|
---|
| 687 | """
|
---|
| 688 | #
|
---|
| 689 | # Deal with the column names first.
|
---|
| 690 | #
|
---|
| 691 | if asRunNames:
|
---|
| 692 | self.asColumns = list(asRunNames);
|
---|
| 693 | else:
|
---|
| 694 | self.asColumns = [];
|
---|
| 695 | iCol = len(self.asColumns);
|
---|
| 696 | while iCol < len(aoTestRuns):
|
---|
| 697 | self.asColumns.append('#%u%s' % (iCol, ' (baseline)' if iCol == self.iBaseline else '',));
|
---|
| 698 |
|
---|
[97279] | 699 | self.aoRows = [
|
---|
| 700 | RunHeaderSeparatorRow(),
|
---|
| 701 | RunHeaderRow('Test / Value', self.asColumns),
|
---|
| 702 | RunHeaderSeparatorRow(),
|
---|
| 703 | ];
|
---|
| 704 |
|
---|
[97266] | 705 | #
|
---|
| 706 | # Now flatten the test trees into a table.
|
---|
| 707 | #
|
---|
| 708 | self.__populateFromRuns([[oTestRun,] for oTestRun in aoTestRuns], 0);
|
---|
[97279] | 709 |
|
---|
| 710 | #
|
---|
| 711 | # Add a footer if there are a lot of rows.
|
---|
| 712 | #
|
---|
| 713 | if len(self.aoRows) - 2 > 40:
|
---|
| 714 | self.aoRows.extend([RunFooterSeparatorRow(), RunFooterRow('', self.asColumns),]);
|
---|
| 715 |
|
---|
[97266] | 716 | return self;
|
---|
| 717 |
|
---|
| 718 | #
|
---|
| 719 | # Text formatting.
|
---|
| 720 | #
|
---|
| 721 |
|
---|
| 722 | def formatAsText(self):
|
---|
| 723 | """
|
---|
| 724 | Formats the table as text.
|
---|
| 725 |
|
---|
| 726 | Returns a string array of the output lines.
|
---|
| 727 | """
|
---|
| 728 |
|
---|
| 729 | #
|
---|
| 730 | # Pass 1: Calculate column widths.
|
---|
| 731 | #
|
---|
| 732 | oWidths = TextWidths(1);
|
---|
| 733 | for oRow in self.aoRows:
|
---|
| 734 | oWidths.update(oRow.calcColumnWidthsForText(self));
|
---|
| 735 | oWidths.finalize();
|
---|
| 736 |
|
---|
| 737 | #
|
---|
| 738 | # Pass 2: Generate the output strings.
|
---|
| 739 | #
|
---|
[97279] | 740 | asRet = [];
|
---|
[97266] | 741 | for oRow in self.aoRows:
|
---|
| 742 | if not oRow.fSkip:
|
---|
[97279] | 743 | asRet.append(oRow.renderAsText(oWidths, self));
|
---|
[97266] | 744 |
|
---|
| 745 | return asRet;
|
---|
| 746 |
|
---|