VirtualBox

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

Last change on this file was 106061, checked in by vboxsync, 3 months ago

Copyright year updates by scm.

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
File size: 27.2 KB
Line 
1# -*- coding: utf-8 -*-
2# $Id: reporting.py 106061 2024-09-16 14:03:52Z 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-2024 Oracle and/or its affiliates.
14
15This file is part of VirtualBox base platform packages, as
16available from https://www.virtualbox.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: 106061 $"
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 def asText(self, cchWidth): # type: (int) -> str
189 """ Pads the text to width of cchWidth characters. """
190 return alignText(self.sText, cchWidth, self.iAlign);
191
192
193class 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
209 def formatNameAsText(self, cchWidth): # (int) -> TextElement
210 """ Format the row as text. """
211 _ = cchWidth;
212 return TextElement(' ' * (self.iLevel * 2) + self.sName, g_kiAlignLeft);
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
227 def calcColumnWidthsForText(self, oTable): # type: (RunTable) -> (bool, int, [])
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]);
243 return (False, len(self.formatNameAsText(0).sText), aoRetCols);
244
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
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
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)),);
284 if round(rdPct, cPctPrecision) != 0:
285 return '%s%.*f%%' % (chSign, cPctPrecision, rdPct,); # %.*f rounds.
286 return '~' + chSign + '0.' + '0' * cPctPrecision + '%';
287
288 @staticmethod
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 = '-';
302 return RunRow.formatPctAsText(chSign, lDiff / float(lBaseline) * 100, cPctPrecision);
303 return '';
304
305
306class 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);
314
315 def formatColumnAsText(self, iColumn, oTable): # type: (int, RunTable) -> [TextElement]
316 return [TextElement(self.asColumns[iColumn], g_kiAlignCenter),];
317
318
319class RunFooterRow(RunHeaderRow):
320 """
321 Run table footer row.
322 """
323 def __init__(self, sName, asColumns):
324 RunHeaderRow.__init__(self, sName, asColumns);
325
326
327class 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
342class 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
354class RunFooterSeparatorRow(RunHeaderSeparatorRow):
355 """
356 Run table footer separator row.
357 """
358 def __init__(self):
359 RunHeaderSeparatorRow.__init__(self);
360
361
362class 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
390class 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
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
406class 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
429class 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;
440 return TextElement('runtime', g_kiAlignRight);
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
463
464class 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
545class 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
594class RunTable(object):
595 """
596 Result table.
597
598 This contains one or more test runs as columns.
599 """
600
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.
609 def __populateFromValues(self, aaoValueRuns, iLevel): # type: ([reader.Value]) -> None
610 """
611 Internal worker for __populateFromRuns()
612
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.
616 """
617 # Same as for __populateFromRuns, only no recursion.
618 fAnalysisRow = False;
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
634 fAnalysisRow = fAnalysisRow or oRow.oValue.canDoBetterCompare();
635 return fAnalysisRow;
636
637 def __populateFromRuns(self, aaoTestRuns, iLevel): # type: ([reader.Test]) -> None
638 """
639 Internal worker for populateFromRuns()
640
641 This will modify the sub-lists inside aaoTestRuns, returning with them all empty.
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
665 # Now recursively do the subtests for it and then do the values.
666 self.__populateFromRuns( [list(oTest.aoChildren) if oTest else list() for oTest in oStartRow.aoTests], iLevel+1);
667 fValueAnalysisRow = self.__populateFromValues([list(oTest.aoValues)
668 if oTest else list() for oTest in oStartRow.aoTests], iLevel+1);
669
670 # Add the end-test row for it.
671 self.aoRows.append(RunTestEndRow(oStartRow));
672 self.aoRows.append(RunTestEndRow2(oStartRow));
673 if fValueAnalysisRow:
674 self.aoRows.append(RunTestValueAnalysisRow(oStartRow));
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
699 self.aoRows = [
700 RunHeaderSeparatorRow(),
701 RunHeaderRow('Test / Value', self.asColumns),
702 RunHeaderSeparatorRow(),
703 ];
704
705 #
706 # Now flatten the test trees into a table.
707 #
708 self.__populateFromRuns([[oTestRun,] for oTestRun in aoTestRuns], 0);
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
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 #
740 asRet = [];
741 for oRow in self.aoRows:
742 if not oRow.fSkip:
743 asRet.append(oRow.renderAsText(oWidths, self));
744
745 return asRet;
746
Note: See TracBrowser for help on using the repository browser.

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