1 | # -*- coding: utf-8 -*-
|
---|
2 | # $Id: reporting.py 97271 2022-10-24 07:55:06Z 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 | """
|
---|
13 | Copyright (C) 2010-2022 Oracle and/or its affiliates.
|
---|
14 |
|
---|
15 | This file is part of VirtualBox base platform packages, as
|
---|
16 | available from https://www.alldomusa.eu.org.
|
---|
17 |
|
---|
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 |
|
---|
31 | The contents of this file may alternatively be used under the terms
|
---|
32 | of the Common Development and Distribution License Version 1.0
|
---|
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
|
---|
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.
|
---|
39 |
|
---|
40 | SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
|
---|
41 | """
|
---|
42 |
|
---|
43 | __version__ = "$Revision: 97271 $"
|
---|
44 |
|
---|
45 | # Standard python imports.
|
---|
46 | import os;
|
---|
47 | import sys;
|
---|
48 |
|
---|
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 |
|
---|
188 |
|
---|
189 | class 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 |
|
---|
287 | class 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 |
|
---|
315 | class 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 |
|
---|
323 | class 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 |
|
---|
346 | class 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 |
|
---|
380 | class 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 |
|
---|
429 | class 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 |
|
---|