VirtualBox

source: vbox/trunk/src/VBox/ValidationKit/analysis/reporting.py@ 97271

最後變更 在這個檔案從97271是 97271,由 vboxsync 提交於 2 年 前

ValKit/analysis: Kicked out old code.

  • 屬性 svn:eol-style 設為 native
  • 屬性 svn:keywords 設為 Author Date Id Revision
檔案大小: 21.8 KB
 
1# -*- coding: utf-8 -*-
2# $Id: reporting.py 97271 2022-10-24 07:55:06Z vboxsync $
3
4"""
5Test Result Report Writer.
6
7This takes a processed test result tree and creates a HTML, re-structured text,
8or normal text report from it.
9"""
10
11__copyright__ = \
12"""
13Copyright (C) 2010-2022 Oracle and/or its affiliates.
14
15This file is part of VirtualBox base platform packages, as
16available from https://www.alldomusa.eu.org.
17
18This program is free software; you can redistribute it and/or
19modify it under the terms of the GNU General Public License
20as published by the Free Software Foundation, in version 3 of the
21License.
22
23This program is distributed in the hope that it will be useful, but
24WITHOUT ANY WARRANTY; without even the implied warranty of
25MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
26General Public License for more details.
27
28You should have received a copy of the GNU General Public License
29along with this program; if not, see <https://www.gnu.org/licenses>.
30
31The contents of this file may alternatively be used under the terms
32of the Common Development and Distribution License Version 1.0
33(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
34in the VirtualBox distribution, in which case the provisions of the
35CDDL are applicable instead of those of the GPL.
36
37You may elect to license modified versions of this file under the
38terms and conditions of either the GPL or the CDDL or both.
39
40SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
41"""
42
43__version__ = "$Revision: 97271 $"
44
45# Standard python imports.
46import os;
47import sys;
48
49# Only the main script needs to modify the path.
50try: __file__;
51except: __file__ = sys.argv[0];
52g_ksValidationKitDir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)));
53sys.path.append(g_ksValidationKitDir);
54
55# ValidationKit imports.
56from common import utils;
57
58# Python 3 hacks:
59if sys.version_info[0] >= 3:
60 long = int; # pylint: disable=redefined-builtin,invalid-name
61
62
63##################################################################################################################################
64# Run Table #
65##################################################################################################################################
66
67def alignTextLeft(sText, cchWidth):
68 """ Left aligns text and pads it to cchWidth characters length. """
69 return sText + ' ' * (cchWidth - min(len(sText), cchWidth));
70
71
72def alignTextRight(sText, cchWidth):
73 """ Right aligns text and pads it to cchWidth characters length. """
74 return ' ' * (cchWidth - min(len(sText), cchWidth)) + sText;
75
76
77def 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
82g_kiAlignLeft = -1;
83g_kiAlignRight = 1;
84g_kiAlignCenter = 0;
85def 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
99class 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
135class 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
179class 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
188
189class RunRow(object):
190 """
191 Run table row.
192 """
193
194 def __init__(self, iLevel, sName, iRun = 0): # type: (int, str, int) -> None
195 self.iLevel = iLevel;
196 self.sName = sName;
197 self.iFirstRun = iRun;
198
199 # Fields used while formatting (set during construction or calcColumnWidthsForText/Html).
200 self.cColumns = 0; ##< Number of columns.
201 self.fSkip = False ##< Whether or not to skip this row in the output.
202
203 # Format as Text:
204
205 def formatNameAsText(self, cchWidth): # (int) -> str
206 """ Format the row as text. """
207 _ = cchWidth;
208 return ' ' * (self.iLevel * 2) + self.sName;
209
210 def getColumnCountAsText(self, oTable):
211 """
212 Called by calcColumnWidthsForText for getting an up-to-date self.cColumns value.
213 Override this to update cColumns after construction.
214 """
215 _ = oTable;
216 return self.cColumns;
217
218 def formatColumnAsText(self, iColumn, oTable): # type: (int, RunTable) -> [TextElement]
219 """ Returns an array of TextElements for the given column in this row. """
220 _ = iColumn; _ = oTable;
221 return [ TextElement(),];
222
223 def calcColumnWidthsForText(self, oTable):
224 """
225 Calculates the column widths for text rendering.
226
227 Returns a tuple consisting of the fSkip, the formatted name width, and an
228 array of column widths. The entries in the latter are either integer
229 widths or arrays of subcolumn integer widths.
230 """
231 aoRetCols = [];
232 cColumns = self.getColumnCountAsText(oTable);
233 for iColumn in range(cColumns):
234 aoSubColumns = self.formatColumnAsText(iColumn, oTable);
235 if len(aoSubColumns) == 1:
236 aoRetCols.append(len(aoSubColumns[0].sText));
237 else:
238 aoRetCols.append([len(oSubColumn.sText) for oSubColumn in aoSubColumns]);
239 return (False, len(self.formatNameAsText(0)) + self.iLevel * 2, aoRetCols);
240
241 @staticmethod
242 def formatDiffAsText(lNumber, lBaseline):
243 """ Formats the difference between lNumber and lBaseline as text. """
244 if lNumber is not None:
245 if lBaseline is not None:
246 if lNumber < lBaseline:
247 return '-' + utils.formatNumber(lBaseline - lNumber); ## @todo formatter is busted for negative nums.
248 if lNumber > lBaseline:
249 return '+' + utils.formatNumber(lNumber - lBaseline);
250 return '0';
251 return '';
252
253 @staticmethod
254 def formatDiffInPctAsText(lNumber, lBaseline, cPctPrecision):
255 """ Formats the difference between lNumber and lBaseline in precent as text. """
256 if lNumber is not None:
257 if lBaseline is not None:
258 ## @todo implement cPctPrecision
259 if lNumber == lBaseline:
260 return '0.' + '0'*cPctPrecision + '%';
261
262 lDiff = lNumber - lBaseline;
263 chSign = '+';
264 if lDiff < 0:
265 lDiff = -lDiff;
266 chSign = '-';
267
268 rdPct = lDiff / float(lBaseline);
269 #if rdPct * 100 >= 5:
270 # return '%s%s%%' % (chSign, utils.formatNumber(int(rdPct * 100 + 0.5)),);
271 #if rdPct * 1000 >= 5:
272 # return u'%s%s\u2030' % (chSign, int(rdPct * 1000 + 0.5),);
273 #if rdPct * 10000 >= 5:
274 # return u'%s%s\u2031' % (chSign, int(rdPct * 10000 + 0.5),);
275 #if rdPct * 1000000 >= 0.5:
276 # return u'%s%sppm' % (chSign, int(rdPct * 1000000 + 0.5),);
277
278 if rdPct * 100 >= 100:
279 return '%s%s%%' % (chSign, utils.formatNumber(int(rdPct * 100 + 0.5)),);
280 if rdPct * 10000 + 0.5 >= 1:
281 return '%s%.*f%%' % (chSign, cPctPrecision, rdPct * 100 + 0.005,);
282
283 return '~' + chSign + '0.' + '0' * cPctPrecision + '%';
284 return '';
285
286
287class RunTestRow(RunRow):
288 """
289 Run table test row.
290 """
291
292 def __init__(self, iLevel, oTest, iRun, aoTests = None): # type: (int, reader.Test, int, [reader.Test]) -> None
293 RunRow.__init__(self, iLevel, oTest.sName, iRun);
294 assert oTest;
295 self.oTest = oTest;
296 if aoTests is None:
297 aoTests = [None for i in range(iRun)];
298 aoTests.append(oTest);
299 else:
300 aoTests= list(aoTests);
301 self.aoTests = aoTests
302
303 def isSameTest(self, oTest):
304 """ Checks if oTest belongs to this row or not. """
305 return oTest.sName == self.oTest.sName;
306
307 def getBaseTest(self, oTable):
308 """ Returns the baseline test. """
309 oBaseTest = self.aoTests[oTable.iBaseline];
310 if not oBaseTest:
311 oBaseTest = self.aoTests[self.iFirstRun];
312 return oBaseTest;
313
314
315class RunTestStartRow(RunTestRow):
316 """
317 Run table start of test row.
318 """
319
320 def __init__(self, iLevel, oTest, iRun): # type: (int, reader.Test, int) -> None
321 RunTestRow.__init__(self, iLevel, oTest, iRun);
322
323class RunTestEndRow(RunTestRow):
324 """
325 Run table end of test row.
326 """
327
328 def __init__(self, oStartRow): # type: (RunTestStartRow) -> None
329 RunTestRow.__init__(self, oStartRow.iLevel, oStartRow.oTest, oStartRow.iFirstRun, oStartRow.aoTests);
330 self.oStartRow = oStartRow # type: RunTestStartRow
331
332 def getColumnCountAsText(self, oTable):
333 self.cColumns = len(self.aoTests);
334 return self.cColumns;
335
336 def formatColumnAsText(self, iColumn, oTable):
337 oTest = self.aoTests[iColumn];
338 if oTest and oTest.sStatus:
339 if oTest.cErrors > 0:
340 return [ TextElement(oTest.sStatus, g_kiAlignCenter),
341 TextElement(utils.formatNumber(oTest.cErrors) + 'errors') ];
342 return [ TextElement(oTest.sStatus, g_kiAlignCenter) ];
343 return [ TextElement(), ];
344
345
346class RunTestEndRow2(RunTestRow):
347 """
348 Run table 2nd end of test row, this shows the times.
349 """
350
351 def __init__(self, oStartRow): # type: (RunTestStartRow) -> None
352 RunTestRow.__init__(self, oStartRow.iLevel, oStartRow.oTest, oStartRow.iFirstRun, oStartRow.aoTests);
353 self.oStartRow = oStartRow # type: RunTestStartRow
354
355 def formatNameAsText(self, cchWidth):
356 _ = cchWidth;
357 return '';
358
359 def getColumnCountAsText(self, oTable):
360 self.cColumns = len(self.aoTests);
361 return self.cColumns;
362
363 def formatColumnAsText(self, iColumn, oTable):
364 oTest = self.aoTests[iColumn];
365 if oTest:
366 cUsElapsed = oTest.calcDurationAsMicroseconds();
367 if cUsElapsed:
368 oBaseTest = self.getBaseTest(oTable);
369 if oTest is oBaseTest:
370 return [ TextElement(utils.formatNumber(cUsElapsed)), TextElement('us', g_kiAlignLeft), ];
371 cUsElapsedBase = oBaseTest.calcDurationAsMicroseconds();
372 aoRet = [
373 TextElement(utils.formatNumber(cUsElapsed)),
374 TextElement(self.formatDiffAsText(cUsElapsed, cUsElapsedBase)),
375 TextElement(self.formatDiffInPctAsText(cUsElapsed, cUsElapsedBase, oTable.cPctPrecision)),
376 ];
377 return aoRet[1:] if oTable.fBrief else aoRet;
378 return [ TextElement(), ];
379
380class RunValueRow(RunRow):
381 """
382 Run table value row.
383 """
384
385 def __init__(self, iLevel, oValue, iRun): # type: (int, reader.Value, int) -> None
386 RunRow.__init__(self, iLevel, oValue.sName, iRun);
387 self.oValue = oValue;
388 self.aoValues = [None for i in range(iRun)];
389 self.aoValues.append(oValue);
390
391 def isSameValue(self, oValue):
392 """ Checks if oValue belongs to this row or not. """
393 return oValue.sName == self.oValue.sName and oValue.sUnit == self.oValue.sUnit;
394
395 # Formatting as Text.
396
397 @staticmethod
398 def formatOneValueAsText(oValue): # type: (reader.Value) -> str
399 """ Formats a value. """
400 if not oValue:
401 return "N/A";
402 return utils.formatNumber(oValue.lValue);
403
404 def getBaseValue(self, oTable):
405 """ Returns the base value instance. """
406 oBaseValue = self.aoValues[oTable.iBaseline];
407 if not oBaseValue:
408 oBaseValue = self.aoValues[self.iFirstRun];
409 return oBaseValue;
410
411 def getColumnCountAsText(self, oTable):
412 self.cColumns = len(self.aoValues);
413 return self.cColumns;
414
415 def formatColumnAsText(self, iColumn, oTable):
416 oValue = self.aoValues[iColumn];
417 oBaseValue = self.getBaseValue(oTable);
418 if oValue is oBaseValue:
419 return [ TextElement(self.formatOneValueAsText(oValue)),
420 TextElement(oValue.sUnit, g_kiAlignLeft), ];
421 aoRet = [
422 TextElement(self.formatOneValueAsText(oValue)),
423 TextElement(self.formatDiffAsText(oValue.lValue if oValue else None, oBaseValue.lValue)),
424 TextElement(self.formatDiffInPctAsText(oValue.lValue if oValue else None, oBaseValue.lValue, oTable.cPctPrecision))
425 ];
426 return aoRet[1:] if oTable.fBrief else aoRet;
427
428
429class RunTable(object):
430 """
431 Result table.
432
433 This contains one or more test runs as columns.
434 """
435
436 def __init__(self, iBaseline = 0, fBrief = True, cPctPrecision = 2): # (int, bool, int) -> None
437 self.asColumns = [] # type: [str] ## Column names.
438 self.aoRows = [] # type: [RunRow] ## The table rows.
439 self.iBaseline = iBaseline # type: int ## Which column is the baseline when diffing things.
440 self.fBrief = fBrief # type: bool ## Whether to exclude the numerical values of non-baseline runs.
441 self.cPctPrecision = cPctPrecision # type: int ## Number of decimal points in diff percentage value.
442
443 def __populateFromValues(self, aaoValueRuns, iLevel): # type: ([reader.Value]) -> None
444 """
445 Internal worker for __populateFromRuns()
446
447 This will modify the sub-lists inside aaoValueRuns, returning with the all empty.
448 """
449 # Same as for __populateFromRuns, only no recursion.
450 for iValueRun, aoValuesForRun in enumerate(aaoValueRuns):
451 while aoValuesForRun:
452 oRow = RunValueRow(iLevel, aoValuesForRun.pop(0), iValueRun);
453 self.aoRows.append(oRow);
454
455 # Pop matching values from the other runs of this test.
456 for iOtherRun in range(iValueRun + 1, len(aaoValueRuns)):
457 aoValuesForOtherRun = aaoValueRuns[iOtherRun];
458 for iValueToPop, oOtherValue in enumerate(aoValuesForOtherRun):
459 if oRow.isSameValue(oOtherValue):
460 oRow.aoValues.append(aoValuesForOtherRun.pop(iValueToPop));
461 break;
462 if len(oRow.aoValues) <= iOtherRun:
463 oRow.aoValues.append(None);
464 return self;
465
466 def __populateFromRuns(self, aaoTestRuns, iLevel): # type: ([reader.Test]) -> None
467 """
468 Internal worker for populateFromRuns()
469
470 This will modify the sub-lists inside aaoTestRuns, returning with the all empty.
471 """
472
473 #
474 # Currently doing depth first, so values are always at the end.
475 # Nominally, we should inject values according to the timestamp.
476 # However, that's too much work right now and can be done later if needed.
477 #
478 for iRun, aoTestForRun in enumerate(aaoTestRuns):
479 while aoTestForRun:
480 # Pop the next test and create a start-test row for it.
481 oStartRow = RunTestStartRow(iLevel, aoTestForRun.pop(0), iRun);
482 self.aoRows.append(oStartRow);
483
484 # Pop matching tests from the other runs.
485 for iOtherRun in range(iRun + 1, len(aaoTestRuns)):
486 aoOtherTestRun = aaoTestRuns[iOtherRun];
487 for iTestToPop, oOtherTest in enumerate(aoOtherTestRun):
488 if oStartRow.isSameTest(oOtherTest):
489 oStartRow.aoTests.append(aoOtherTestRun.pop(iTestToPop));
490 break;
491 if len(oStartRow.aoTests) <= iOtherRun:
492 oStartRow.aoTests.append(None);
493
494 # Now recrusively do the subtests for it and then do the values.
495 self.__populateFromRuns( [list(oTest.aoChildren) if oTest else list() for oTest in oStartRow.aoTests], iLevel+1);
496 self.__populateFromValues([list(oTest.aoValues) if oTest else list() for oTest in oStartRow.aoTests], iLevel+1);
497
498 # Add the end-test row for it.
499 self.aoRows.append(RunTestEndRow(oStartRow));
500 self.aoRows.append(RunTestEndRow2(oStartRow));
501
502 return self;
503
504 def populateFromRuns(self, aoTestRuns, asRunNames = None): # type: ([reader.Test], [str]) -> RunTable
505 """
506 Populates the table from the series of runs.
507
508 The aoTestRuns and asRunNames run in parallel. If the latter isn't
509 given, the names will just be ordinals starting with #0 for the
510 first column.
511
512 Returns self.
513 """
514 #
515 # Deal with the column names first.
516 #
517 if asRunNames:
518 self.asColumns = list(asRunNames);
519 else:
520 self.asColumns = [];
521 iCol = len(self.asColumns);
522 while iCol < len(aoTestRuns):
523 self.asColumns.append('#%u%s' % (iCol, ' (baseline)' if iCol == self.iBaseline else '',));
524
525 #
526 # Now flatten the test trees into a table.
527 #
528 self.__populateFromRuns([[oTestRun,] for oTestRun in aoTestRuns], 0);
529 return self;
530
531 #
532 # Text formatting.
533 #
534
535 def formatAsText(self):
536 """
537 Formats the table as text.
538
539 Returns a string array of the output lines.
540 """
541
542 #
543 # Pass 1: Calculate column widths.
544 #
545 oWidths = TextWidths(1);
546 for oRow in self.aoRows:
547 oWidths.update(oRow.calcColumnWidthsForText(self));
548 oWidths.finalize();
549
550 #
551 # Pass 2: Generate the output strings.
552 #
553 # Header
554 asRet = [
555 alignTextCenter('Test / Value', oWidths.cchName) + ': '
556 + ' | '.join([alignTextCenter(sText, oWidths.getColumnWidth(iCol)) for iCol, sText in enumerate(self.asColumns)]),
557 ];
558 asRet.append('=' * len(asRet[0]));
559
560 # The table
561 for oRow in self.aoRows:
562 if not oRow.fSkip:
563 sRow = oRow.formatNameAsText(oWidths.cchName);
564 sRow = sRow + ' ' * (oWidths.cchName - min(len(sRow), oWidths.cchName)) + ': ';
565
566 for iColumn in range(oRow.cColumns):
567 aoSubCols = oRow.formatColumnAsText(iColumn, self);
568 sCell = '';
569 for iSub, oText in enumerate(aoSubCols):
570 cchWidth = oWidths.getColumnWidth(iColumn, len(aoSubCols), iSub);
571 if iSub > 0:
572 sCell += ' ' * oWidths.cchSubColSpacing;
573 sCell += alignText(oText.sText, cchWidth, oText.iAlign);
574 cchWidth = oWidths.getColumnWidth(iColumn);
575 sRow += (' | ' if iColumn > 0 else ' ') + ' ' * (cchWidth - min(cchWidth, len(sCell))) + sCell;
576
577 asRet.append(sRow);
578
579 # Footer?
580 if len(asRet) > 40:
581 asRet.append(asRet[1]);
582 asRet.append(asRet[0]);
583
584 return asRet;
585
注意: 瀏覽 TracBrowser 來幫助您使用儲存庫瀏覽器

© 2025 Oracle Support Privacy / Do Not Sell My Info Terms of Use Trademark Policy Automated Access Etiquette