1 | # -*- coding: utf-8 -*-
|
---|
2 | # $Id: reader.py 97266 2022-10-23 00:06:26Z vboxsync $
|
---|
3 |
|
---|
4 | """
|
---|
5 | XML reader module.
|
---|
6 |
|
---|
7 | This produces a test result tree that can be processed and passed to
|
---|
8 | reporting.
|
---|
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.215389.xyz.
|
---|
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 | __version__ = "$Revision: 97266 $"
|
---|
43 | __all__ = [ 'parseTestResult', ]
|
---|
44 |
|
---|
45 | # Standard python imports.
|
---|
46 | import datetime;
|
---|
47 | import os;
|
---|
48 | import sys;
|
---|
49 | import traceback;
|
---|
50 |
|
---|
51 | # Only the main script needs to modify the path.
|
---|
52 | try: __file__;
|
---|
53 | except: __file__ = sys.argv[0];
|
---|
54 | g_ksValidationKitDir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)));
|
---|
55 | sys.path.append(g_ksValidationKitDir);
|
---|
56 |
|
---|
57 | # ValidationKit imports.
|
---|
58 | from common import utils;
|
---|
59 |
|
---|
60 | # Python 3 hacks:
|
---|
61 | if sys.version_info[0] >= 3:
|
---|
62 | long = int; # pylint: disable=redefined-builtin,invalid-name
|
---|
63 |
|
---|
64 | # pylint: disable=missing-docstring
|
---|
65 |
|
---|
66 |
|
---|
67 | class Value(object):
|
---|
68 | """
|
---|
69 | Represents a value. Usually this is benchmark result or parameter.
|
---|
70 | """
|
---|
71 |
|
---|
72 | kdBestByUnit = {
|
---|
73 | "%": +1, # Difficult to say what's best really.
|
---|
74 | "bytes": +1, # Difficult to say what's best really.
|
---|
75 | "bytes/s": +2,
|
---|
76 | "KB": +1,
|
---|
77 | "KB/s": +2,
|
---|
78 | "MB": +1,
|
---|
79 | "MB/s": +2,
|
---|
80 | "packets": +2,
|
---|
81 | "packets/s": +2,
|
---|
82 | "frames": +2,
|
---|
83 | "frames/s": +2,
|
---|
84 | "occurrences": +1, # Difficult to say what's best really.
|
---|
85 | "occurrences/s": +2,
|
---|
86 | "roundtrips": +2,
|
---|
87 | "calls": +1, # Difficult to say what's best really.
|
---|
88 | "calls/s": +2,
|
---|
89 | "s": -2,
|
---|
90 | "ms": -2,
|
---|
91 | "ns": -2,
|
---|
92 | "ns/call": -2,
|
---|
93 | "ns/frame": -2,
|
---|
94 | "ns/occurrence": -2,
|
---|
95 | "ns/packet": -2,
|
---|
96 | "ns/roundtrip": -2,
|
---|
97 | "ins": +2,
|
---|
98 | "ins/sec": -1,
|
---|
99 | "": +1, # Difficult to say what's best really.
|
---|
100 | "pp1k": -2,
|
---|
101 | "pp10k": -2,
|
---|
102 | "ppm": -2,
|
---|
103 | "ppb": -2,
|
---|
104 | "ticks": -1, # Difficult to say what's best really.
|
---|
105 | "ticks/call": -2,
|
---|
106 | "ticks/occ": -2,
|
---|
107 | "pages": +1, # Difficult to say what's best really.
|
---|
108 | "pages/s": +2,
|
---|
109 | "ticks/page": -2,
|
---|
110 | "ns/page": -2,
|
---|
111 | "ps": -1, # Difficult to say what's best really.
|
---|
112 | "ps/call": -2,
|
---|
113 | "ps/frame": -2,
|
---|
114 | "ps/occurrence": -2,
|
---|
115 | "ps/packet": -2,
|
---|
116 | "ps/roundtrip": -2,
|
---|
117 | "ps/page": -2,
|
---|
118 | };
|
---|
119 |
|
---|
120 | def __init__(self, oTest, sName = None, sUnit = None, sTimestamp = None, lValue = None):
|
---|
121 | self.oTest = oTest;
|
---|
122 | self.sName = sName;
|
---|
123 | self.sUnit = sUnit;
|
---|
124 | self.sTimestamp = sTimestamp;
|
---|
125 | self.lValue = self.valueToInteger(lValue);
|
---|
126 | assert self.lValue is None or isinstance(self.lValue, (int, long)), "lValue=%s %s" % (self.lValue, type(self.lValue),);
|
---|
127 |
|
---|
128 | # Members set by processing.
|
---|
129 | self.sDiff = None;
|
---|
130 |
|
---|
131 | def clone(self, oParentTest):
|
---|
132 | """
|
---|
133 | Clones the value.
|
---|
134 | """
|
---|
135 | return Value(oParentTest, self.sName, self.sUnit, self.sTimestamp, self.lValue);
|
---|
136 |
|
---|
137 |
|
---|
138 | @staticmethod
|
---|
139 | def valueToInteger(sValue):
|
---|
140 | """
|
---|
141 | Returns integer (long) represention of lValue.
|
---|
142 | Returns None if it cannot be converted to integer.
|
---|
143 |
|
---|
144 | Raises an exception if sValue isn't an integer.
|
---|
145 | """
|
---|
146 | if sValue is None or isinstance(sValue, (int, long)):
|
---|
147 | return sValue;
|
---|
148 | sValue = sValue.strip();
|
---|
149 | if not sValue:
|
---|
150 | return None;
|
---|
151 | return long(sValue);
|
---|
152 |
|
---|
153 | # Manipluation
|
---|
154 |
|
---|
155 | def distill(self, aoValues, sMethod):
|
---|
156 | """
|
---|
157 | Distills the value of the object from values from multiple test runs.
|
---|
158 | """
|
---|
159 | if not aoValues:
|
---|
160 | return self;
|
---|
161 |
|
---|
162 | # Everything except the value comes from the first run.
|
---|
163 | self.sName = aoValues[0].sName;
|
---|
164 | self.sTimestamp = aoValues[0].sTimestamp;
|
---|
165 | self.sUnit = aoValues[0].sUnit;
|
---|
166 |
|
---|
167 | # Find the value to use according to sMethod.
|
---|
168 | if len(aoValues) == 1:
|
---|
169 | self.lValue = aoValues[0].lValue;
|
---|
170 | else:
|
---|
171 | alValuesXcptInvalid = [oValue.lValue for oValue in aoValues if oValue.lValue is not None];
|
---|
172 | if not alValuesXcptInvalid:
|
---|
173 | # No integer result, so just pick the first value whatever it is.
|
---|
174 | self.lValue = aoValues[0].lValue;
|
---|
175 |
|
---|
176 | elif sMethod == 'best':
|
---|
177 | # Pick the best result out of the whole bunch.
|
---|
178 | if self.kdBestByUnit[self.sUnit] >= 0:
|
---|
179 | self.lValue = max(alValuesXcptInvalid);
|
---|
180 | else:
|
---|
181 | self.lValue = min(alValuesXcptInvalid);
|
---|
182 |
|
---|
183 | elif sMethod == 'avg':
|
---|
184 | # Calculate the average.
|
---|
185 | self.lValue = (sum(alValuesXcptInvalid) + len(alValuesXcptInvalid) // 2) // len(alValuesXcptInvalid);
|
---|
186 |
|
---|
187 | else:
|
---|
188 | assert False;
|
---|
189 | self.lValue = aoValues[0].lValue;
|
---|
190 |
|
---|
191 | return self;
|
---|
192 |
|
---|
193 |
|
---|
194 | # debug
|
---|
195 |
|
---|
196 | def printValue(self, cIndent):
|
---|
197 | print('%sValue: name=%s timestamp=%s unit=%s value=%s'
|
---|
198 | % (''.ljust(cIndent*2), self.sName, self.sTimestamp, self.sUnit, self.lValue));
|
---|
199 |
|
---|
200 |
|
---|
201 | class Test(object):
|
---|
202 | """
|
---|
203 | Nested test result.
|
---|
204 | """
|
---|
205 | def __init__(self, oParent = None, hsAttrs = None):
|
---|
206 | self.aoChildren = [] # type: list(Test)
|
---|
207 | self.aoValues = [];
|
---|
208 | self.oParent = oParent;
|
---|
209 | self.sName = hsAttrs['name'] if hsAttrs else None;
|
---|
210 | self.sStartTS = hsAttrs['timestamp'] if hsAttrs else None;
|
---|
211 | self.sEndTS = None;
|
---|
212 | self.sStatus = None;
|
---|
213 | self.cErrors = -1;
|
---|
214 |
|
---|
215 | # Members set by processing.
|
---|
216 | self.sStatusDiff = None;
|
---|
217 | self.cErrorsDiff = None;
|
---|
218 |
|
---|
219 | def clone(self, oParent = None):
|
---|
220 | """
|
---|
221 | Returns a deep copy.
|
---|
222 | """
|
---|
223 | oClone = Test(oParent, {'name': self.sName, 'timestamp': self.sStartTS});
|
---|
224 |
|
---|
225 | for oChild in self.aoChildren:
|
---|
226 | oClone.aoChildren.append(oChild.clone(oClone));
|
---|
227 |
|
---|
228 | for oValue in self.aoValues:
|
---|
229 | oClone.aoValues.append(oValue.clone(oClone));
|
---|
230 |
|
---|
231 | oClone.sEndTS = self.sEndTS;
|
---|
232 | oClone.sStatus = self.sStatus;
|
---|
233 | oClone.cErrors = self.cErrors;
|
---|
234 | return oClone;
|
---|
235 |
|
---|
236 | # parsing
|
---|
237 |
|
---|
238 | def addChild(self, oChild):
|
---|
239 | self.aoChildren.append(oChild);
|
---|
240 | return oChild;
|
---|
241 |
|
---|
242 | def addValue(self, oValue):
|
---|
243 | self.aoValues.append(oValue);
|
---|
244 | return oValue;
|
---|
245 |
|
---|
246 | def __markCompleted(self, sTimestamp):
|
---|
247 | """ Sets sEndTS if not already done. """
|
---|
248 | if not self.sEndTS:
|
---|
249 | self.sEndTS = sTimestamp;
|
---|
250 |
|
---|
251 | def markPassed(self, sTimestamp):
|
---|
252 | self.__markCompleted(sTimestamp);
|
---|
253 | self.sStatus = 'passed';
|
---|
254 | self.cErrors = 0;
|
---|
255 |
|
---|
256 | def markSkipped(self, sTimestamp):
|
---|
257 | self.__markCompleted(sTimestamp);
|
---|
258 | self.sStatus = 'skipped';
|
---|
259 | self.cErrors = 0;
|
---|
260 |
|
---|
261 | def markFailed(self, sTimestamp, cErrors):
|
---|
262 | self.__markCompleted(sTimestamp);
|
---|
263 | self.sStatus = 'failed';
|
---|
264 | self.cErrors = cErrors;
|
---|
265 |
|
---|
266 | def markEnd(self, sTimestamp, cErrors):
|
---|
267 | self.__markCompleted(sTimestamp);
|
---|
268 | if self.sStatus is None:
|
---|
269 | self.sStatus = 'failed' if cErrors != 0 else 'end';
|
---|
270 | self.cErrors = 0;
|
---|
271 |
|
---|
272 | def mergeInIncludedTest(self, oTest):
|
---|
273 | """ oTest will be robbed. """
|
---|
274 | if oTest is not None:
|
---|
275 | for oChild in oTest.aoChildren:
|
---|
276 | oChild.oParent = self;
|
---|
277 | self.aoChildren.append(oChild);
|
---|
278 | for oValue in oTest.aoValues:
|
---|
279 | oValue.oTest = self;
|
---|
280 | self.aoValues.append(oValue);
|
---|
281 | oTest.aoChildren = [];
|
---|
282 | oTest.aoValues = [];
|
---|
283 |
|
---|
284 | # debug
|
---|
285 |
|
---|
286 | def printTree(self, iLevel = 0):
|
---|
287 | print('%sTest: name=%s start=%s end=%s' % (''.ljust(iLevel*2), self.sName, self.sStartTS, self.sEndTS));
|
---|
288 | for oChild in self.aoChildren:
|
---|
289 | oChild.printTree(iLevel + 1);
|
---|
290 | for oValue in self.aoValues:
|
---|
291 | oValue.printValue(iLevel + 1);
|
---|
292 |
|
---|
293 | # getters / queries
|
---|
294 |
|
---|
295 | def getFullNameWorker(self, cSkipUpper):
|
---|
296 | if self.oParent is None:
|
---|
297 | return (self.sName, 0);
|
---|
298 | sName, iLevel = self.oParent.getFullNameWorker(cSkipUpper);
|
---|
299 | if iLevel < cSkipUpper:
|
---|
300 | sName = self.sName;
|
---|
301 | else:
|
---|
302 | sName += ', ' + self.sName;
|
---|
303 | return (sName, iLevel + 1);
|
---|
304 |
|
---|
305 | def getFullName(self, cSkipUpper = 2):
|
---|
306 | return self.getFullNameWorker(cSkipUpper)[0];
|
---|
307 |
|
---|
308 | def matchFilters(self, asFilters):
|
---|
309 | """
|
---|
310 | Checks if the all of the specified filter strings are substrings
|
---|
311 | of the full test name. Returns True / False.
|
---|
312 | """
|
---|
313 | sName = self.getFullName();
|
---|
314 | for sFilter in asFilters:
|
---|
315 | if sName.find(sFilter) < 0:
|
---|
316 | return False;
|
---|
317 | return True;
|
---|
318 |
|
---|
319 | # manipulation
|
---|
320 |
|
---|
321 | def filterTestsWorker(self, asFilters):
|
---|
322 | # depth first
|
---|
323 | i = 0;
|
---|
324 | while i < len(self.aoChildren):
|
---|
325 | if self.aoChildren[i].filterTestsWorker(asFilters):
|
---|
326 | i += 1;
|
---|
327 | else:
|
---|
328 | self.aoChildren[i].oParent = None;
|
---|
329 | del self.aoChildren[i];
|
---|
330 |
|
---|
331 | # If we have children, they must've matched up.
|
---|
332 | if self.aoChildren:
|
---|
333 | return True;
|
---|
334 | return self.matchFilters(asFilters);
|
---|
335 |
|
---|
336 | def filterTests(self, asFilters):
|
---|
337 | if asFilters:
|
---|
338 | self.filterTestsWorker(asFilters)
|
---|
339 | return self;
|
---|
340 |
|
---|
341 | @staticmethod
|
---|
342 | def calcDurationStatic(sStartTS, sEndTS):
|
---|
343 | """
|
---|
344 | Returns None the start timestamp is absent or invalid.
|
---|
345 | Returns datetime.timedelta otherwise.
|
---|
346 | """
|
---|
347 | if not sStartTS:
|
---|
348 | return None;
|
---|
349 | try:
|
---|
350 | oStart = utils.parseIsoTimestamp(sStartTS);
|
---|
351 | except:
|
---|
352 | return None;
|
---|
353 |
|
---|
354 | if not sEndTS:
|
---|
355 | return datetime.timedelta.max;
|
---|
356 | try:
|
---|
357 | oEnd = utils.parseIsoTimestamp(sEndTS);
|
---|
358 | except:
|
---|
359 | return datetime.timedelta.max;
|
---|
360 |
|
---|
361 | return oEnd - oStart;
|
---|
362 |
|
---|
363 | def calcDuration(self):
|
---|
364 | """
|
---|
365 | Returns the duration as a datetime.timedelta object or None if not available.
|
---|
366 | """
|
---|
367 | return self.calcDurationStatic(self.sStartTS, self.sEndTS);
|
---|
368 |
|
---|
369 | def calcDurationAsMicroseconds(self):
|
---|
370 | """
|
---|
371 | Returns the duration as microseconds or None if not available.
|
---|
372 | """
|
---|
373 | oDuration = self.calcDuration();
|
---|
374 | if not oDuration:
|
---|
375 | return None;
|
---|
376 | return (oDuration.days * 86400 + oDuration.seconds) * 1000000 + oDuration.microseconds;
|
---|
377 |
|
---|
378 | @staticmethod
|
---|
379 | def distillTimes(aoTestRuns, sMethod, sStatus):
|
---|
380 | """
|
---|
381 | Destills the error counts of the tests.
|
---|
382 | Returns a (sStartTS, sEndTS) pair.
|
---|
383 | """
|
---|
384 |
|
---|
385 | #
|
---|
386 | # Start by assembling two list of start and end times for all runs that have a start timestamp.
|
---|
387 | # Then sort out the special cases where no run has a start timestamp and only a single one has.
|
---|
388 | #
|
---|
389 | asStartTS = [oRun.sStartTS for oRun in aoTestRuns if oRun.sStartTS];
|
---|
390 | if not asStartTS:
|
---|
391 | return (None, None);
|
---|
392 | asEndTS = [oRun.sEndTS for oRun in aoTestRuns if oRun.sStartTS]; # parallel to asStartTS, so we don't check sEndTS.
|
---|
393 | if len(asStartTS) == 1:
|
---|
394 | return (asStartTS[0], asEndTS[0]);
|
---|
395 |
|
---|
396 | #
|
---|
397 | # Calculate durations for all runs.
|
---|
398 | #
|
---|
399 | if sMethod == 'best':
|
---|
400 | aoDurations = [Test.calcDurationStatic(oRun.sStartTS, oRun.sEndTS) for oRun in aoTestRuns if oRun.sStatus == sStatus];
|
---|
401 | if not aoDurations or aoDurations.count(None) == len(aoDurations):
|
---|
402 | aoDurations = [Test.calcDurationStatic(oRun.sStartTS, oRun.sEndTS) for oRun in aoTestRuns];
|
---|
403 | if aoDurations.count(None) == len(aoDurations):
|
---|
404 | return (asStartTS[0], None);
|
---|
405 | oDuration = min([oDuration for oDuration in aoDurations if oDuration is not None]);
|
---|
406 |
|
---|
407 | elif sMethod == 'avg':
|
---|
408 | print("dbg: 0: sStatus=%s []=%s"
|
---|
409 | % (sStatus, [(Test.calcDurationStatic(oRun.sStartTS, oRun.sEndTS),oRun.sStatus) for oRun in aoTestRuns],));
|
---|
410 | aoDurations = [Test.calcDurationStatic(oRun.sStartTS, oRun.sEndTS) for oRun in aoTestRuns if oRun.sStatus == sStatus];
|
---|
411 | print("dbg: 1: aoDurations=%s" % (aoDurations,))
|
---|
412 | aoDurations = [oDuration for oDuration in aoDurations if oDuration];
|
---|
413 | print("dbg: 2: aoDurations=%s" % (aoDurations,))
|
---|
414 | if not aoDurations:
|
---|
415 | return (asStartTS[0], None);
|
---|
416 | aoDurations = [oDuration for oDuration in aoDurations if oDuration < datetime.timedelta.max];
|
---|
417 | print("dbg: 3: aoDurations=%s" % (aoDurations,))
|
---|
418 | if not aoDurations:
|
---|
419 | return (asStartTS[0], None);
|
---|
420 | # sum doesn't work on timedelta, so do it manually.
|
---|
421 | oDuration = aoDurations[0];
|
---|
422 | for i in range(1, len(aoDurations)):
|
---|
423 | oDuration += aoDurations[i];
|
---|
424 | print("dbg: 5: oDuration=%s" % (aoDurations,))
|
---|
425 | oDuration = oDuration / len(aoDurations);
|
---|
426 | print("dbg: 6: oDuration=%s" % (aoDurations,))
|
---|
427 |
|
---|
428 | else:
|
---|
429 | assert False;
|
---|
430 | return (asStartTS[0], asEndTS[0]);
|
---|
431 |
|
---|
432 | # Check unfinished test.
|
---|
433 | if oDuration >= datetime.timedelta.max:
|
---|
434 | return (asStartTS[0], None);
|
---|
435 |
|
---|
436 | # Calculate and format the end timestamp string.
|
---|
437 | oStartTS = utils.parseIsoTimestamp(asStartTS[0]);
|
---|
438 | oEndTS = oStartTS + oDuration;
|
---|
439 | return (asStartTS[0], utils.formatIsoTimestamp(oEndTS));
|
---|
440 |
|
---|
441 | @staticmethod
|
---|
442 | def distillStatus(aoTestRuns, sMethod):
|
---|
443 | """
|
---|
444 | Destills the status of the tests.
|
---|
445 | Returns the status.
|
---|
446 | """
|
---|
447 | asStatuses = [oRun.sStatus for oRun in aoTestRuns];
|
---|
448 |
|
---|
449 | if sMethod == 'best':
|
---|
450 | for sStatus in ('passed', 'failed', 'skipped'):
|
---|
451 | if sStatus in asStatuses:
|
---|
452 | return sStatus;
|
---|
453 | return asStatuses[0];
|
---|
454 |
|
---|
455 | if sMethod == 'avg':
|
---|
456 | cPassed = asStatuses.count('passed');
|
---|
457 | cFailed = asStatuses.count('failed');
|
---|
458 | cSkipped = asStatuses.count('skipped');
|
---|
459 | cEnd = asStatuses.count('end');
|
---|
460 | cNone = asStatuses.count(None);
|
---|
461 | if cPassed >= cFailed and cPassed >= cSkipped and cPassed >= cNone and cPassed >= cEnd:
|
---|
462 | return 'passed';
|
---|
463 | if cFailed >= cPassed and cFailed >= cSkipped and cFailed >= cNone and cFailed >= cEnd:
|
---|
464 | return 'failed';
|
---|
465 | if cSkipped >= cPassed and cSkipped >= cFailed and cSkipped >= cNone and cSkipped >= cEnd:
|
---|
466 | return 'skipped';
|
---|
467 | if cEnd >= cPassed and cEnd >= cFailed and cEnd >= cNone and cEnd >= cSkipped:
|
---|
468 | return 'end';
|
---|
469 | return None;
|
---|
470 |
|
---|
471 | assert False;
|
---|
472 | return asStatuses[0];
|
---|
473 |
|
---|
474 | @staticmethod
|
---|
475 | def distillErrors(aoTestRuns, sMethod):
|
---|
476 | """
|
---|
477 | Destills the error counts of the tests.
|
---|
478 | Returns the status.
|
---|
479 | """
|
---|
480 | acErrorsXcptNeg = [oRun.cErrors for oRun in aoTestRuns if oRun.cErrors >= 0];
|
---|
481 |
|
---|
482 | if sMethod == 'best':
|
---|
483 | if acErrorsXcptNeg:
|
---|
484 | return min(acErrorsXcptNeg);
|
---|
485 | elif sMethod == 'avg':
|
---|
486 | if acErrorsXcptNeg:
|
---|
487 | return sum(acErrorsXcptNeg) // len(acErrorsXcptNeg);
|
---|
488 | else:
|
---|
489 | assert False;
|
---|
490 | return -1;
|
---|
491 |
|
---|
492 | def distill(self, aoTestRuns, sMethod, fDropLoners):
|
---|
493 | """
|
---|
494 | Distills the test runs into this test.
|
---|
495 | """
|
---|
496 | #
|
---|
497 | # Recurse first (before we create too much state in the stack
|
---|
498 | # frame) and do child tests.
|
---|
499 | #
|
---|
500 | # We copy the child lists of each test run so we can remove tests we've
|
---|
501 | # processed from each run and thus make sure we include tests in
|
---|
502 | #
|
---|
503 | #
|
---|
504 | aaoChildren = [list(oRun.aoChildren) for oRun in aoTestRuns];
|
---|
505 |
|
---|
506 | # Process the tests for each run.
|
---|
507 | for i, _ in enumerate(aaoChildren):
|
---|
508 | # Process all tests for the current run.
|
---|
509 | while len(aaoChildren[i]) > 0:
|
---|
510 | oFirst = aaoChildren[i].pop(0);
|
---|
511 |
|
---|
512 | # Build a list of sub-test runs by searching remaining runs by test name.
|
---|
513 | aoSameSubTests = [oFirst,];
|
---|
514 | for j in range(i + 1, len(aaoChildren)):
|
---|
515 | aoThis = aaoChildren[j];
|
---|
516 | for iThis, oThis in enumerate(aoThis):
|
---|
517 | if oThis.sName == oFirst.sName:
|
---|
518 | del aoThis[iThis];
|
---|
519 | aoSameSubTests.append(oThis);
|
---|
520 | break;
|
---|
521 |
|
---|
522 | # Apply fDropLoners.
|
---|
523 | if not fDropLoners or len(aoSameSubTests) > 1 or len(aaoChildren) == 1:
|
---|
524 | # Create an empty test and call distill on it with the subtest array, unless
|
---|
525 | # of course that the array only has one member and we can simply clone it.
|
---|
526 | if len(aoSameSubTests) == 1:
|
---|
527 | self.addChild(oFirst.clone(self));
|
---|
528 | else:
|
---|
529 | oSubTest = Test(self);
|
---|
530 | oSubTest.sName = oFirst.sName;
|
---|
531 | oSubTest.distill(aoSameSubTests, sMethod, fDropLoners);
|
---|
532 | self.addChild(oSubTest);
|
---|
533 | del aaoChildren;
|
---|
534 |
|
---|
535 | #
|
---|
536 | # Do values. Similar approch as for the sub-tests.
|
---|
537 | #
|
---|
538 | aaoValues = [list(oRun.aoValues) for oRun in aoTestRuns];
|
---|
539 |
|
---|
540 | # Process the values for each run.
|
---|
541 | for i,_ in enumerate(aaoValues):
|
---|
542 | # Process all values for the current run.
|
---|
543 | while len(aaoValues[i]) > 0:
|
---|
544 | oFirst = aaoValues[i].pop(0);
|
---|
545 |
|
---|
546 | # Build a list of values runs by searching remaining runs by value name and unit.
|
---|
547 | aoSameValues = [oFirst,];
|
---|
548 | for j in range(i + 1, len(aaoValues)):
|
---|
549 | aoThis = aaoValues[j];
|
---|
550 | for iThis, oThis in enumerate(aoThis):
|
---|
551 | if oThis.sName == oFirst.sName and oThis.sUnit == oFirst.sUnit:
|
---|
552 | del aoThis[iThis];
|
---|
553 | aoSameValues.append(oThis);
|
---|
554 | break;
|
---|
555 |
|
---|
556 | # Apply fDropLoners.
|
---|
557 | if not fDropLoners or len(aoSameValues) > 1 or len(aaoValues) == 1:
|
---|
558 | # Create an empty test and call distill on it with the subtest array, unless
|
---|
559 | # of course that the array only has one member and we can simply clone it.
|
---|
560 | if len(aoSameValues) == 1:
|
---|
561 | self.aoValues.append(oFirst.clone(self));
|
---|
562 | else:
|
---|
563 | oValue = Value(self);
|
---|
564 | oValue.distill(aoSameValues, sMethod);
|
---|
565 | self.aoValues.append(oValue);
|
---|
566 | del aaoValues;
|
---|
567 |
|
---|
568 | #
|
---|
569 | # Distill test properties.
|
---|
570 | #
|
---|
571 | self.sStatus = self.distillStatus(aoTestRuns, sMethod);
|
---|
572 | self.cErrors = self.distillErrors(aoTestRuns, sMethod);
|
---|
573 | (self.sStartTS, self.sEndTS) = self.distillTimes(aoTestRuns, sMethod, self.sStatus);
|
---|
574 | print("dbg: %s: sStartTS=%s, sEndTS=%s" % (self.sName, self.sStartTS, self.sEndTS));
|
---|
575 |
|
---|
576 | return self;
|
---|
577 |
|
---|
578 |
|
---|
579 | class XmlLogReader(object):
|
---|
580 | """
|
---|
581 | XML log reader class.
|
---|
582 | """
|
---|
583 |
|
---|
584 | def __init__(self, sXmlFile):
|
---|
585 | self.sXmlFile = sXmlFile;
|
---|
586 | self.oRoot = Test(None, {'name': 'root', 'timestamp': ''});
|
---|
587 | self.oTest = self.oRoot;
|
---|
588 | self.iLevel = 0;
|
---|
589 | self.oValue = None;
|
---|
590 |
|
---|
591 | def parse(self):
|
---|
592 | try:
|
---|
593 | oFile = open(self.sXmlFile, 'rb'); # pylint: disable=consider-using-with
|
---|
594 | except:
|
---|
595 | traceback.print_exc();
|
---|
596 | return False;
|
---|
597 |
|
---|
598 | from xml.parsers.expat import ParserCreate
|
---|
599 | oParser = ParserCreate();
|
---|
600 | oParser.StartElementHandler = self.handleElementStart;
|
---|
601 | oParser.CharacterDataHandler = self.handleElementData;
|
---|
602 | oParser.EndElementHandler = self.handleElementEnd;
|
---|
603 | try:
|
---|
604 | oParser.ParseFile(oFile);
|
---|
605 | except:
|
---|
606 | traceback.print_exc();
|
---|
607 | oFile.close();
|
---|
608 | return False;
|
---|
609 | oFile.close();
|
---|
610 | return True;
|
---|
611 |
|
---|
612 | def handleElementStart(self, sName, hsAttrs):
|
---|
613 | #print('%s%s: %s' % (''.ljust(self.iLevel * 2), sName, str(hsAttrs)));
|
---|
614 | if sName in ('Test', 'SubTest',):
|
---|
615 | self.iLevel += 1;
|
---|
616 | self.oTest = self.oTest.addChild(Test(self.oTest, hsAttrs));
|
---|
617 | elif sName == 'Value':
|
---|
618 | self.oValue = self.oTest.addValue(Value(self.oTest, hsAttrs.get('name'), hsAttrs.get('unit'),
|
---|
619 | hsAttrs.get('timestamp'), hsAttrs.get('value')));
|
---|
620 | elif sName == 'End':
|
---|
621 | self.oTest.markEnd(hsAttrs.get('timestamp'), int(hsAttrs.get('errors', '0')));
|
---|
622 | elif sName == 'Passed':
|
---|
623 | self.oTest.markPassed(hsAttrs.get('timestamp'));
|
---|
624 | elif sName == 'Skipped':
|
---|
625 | self.oTest.markSkipped(hsAttrs.get('timestamp'));
|
---|
626 | elif sName == 'Failed':
|
---|
627 | self.oTest.markFailed(hsAttrs.get('timestamp'), int(hsAttrs['errors']));
|
---|
628 | elif sName == 'Include':
|
---|
629 | self.handleInclude(hsAttrs);
|
---|
630 | else:
|
---|
631 | print('Unknown element "%s"' % (sName,));
|
---|
632 |
|
---|
633 | def handleElementData(self, sData):
|
---|
634 | if self.oValue is not None:
|
---|
635 | self.oValue.addData(sData);
|
---|
636 | elif sData.strip() != '':
|
---|
637 | print('Unexpected data "%s"' % (sData,));
|
---|
638 | return True;
|
---|
639 |
|
---|
640 | def handleElementEnd(self, sName):
|
---|
641 | if sName in ('Test', 'Subtest',):
|
---|
642 | self.iLevel -= 1;
|
---|
643 | self.oTest = self.oTest.oParent;
|
---|
644 | elif sName == 'Value':
|
---|
645 | self.oValue = None;
|
---|
646 | return True;
|
---|
647 |
|
---|
648 | def handleInclude(self, hsAttrs):
|
---|
649 | # relative or absolute path.
|
---|
650 | sXmlFile = hsAttrs['filename'];
|
---|
651 | if not os.path.isabs(sXmlFile):
|
---|
652 | sXmlFile = os.path.join(os.path.dirname(self.sXmlFile), sXmlFile);
|
---|
653 |
|
---|
654 | # Try parse it.
|
---|
655 | oSub = parseTestResult(sXmlFile);
|
---|
656 | if oSub is None:
|
---|
657 | print('error: failed to parse include "%s"' % (sXmlFile,));
|
---|
658 | else:
|
---|
659 | # Skip the root and the next level before merging it the subtest and
|
---|
660 | # values in to the current test. The reason for this is that the
|
---|
661 | # include is the output of some sub-program we've run and we don't need
|
---|
662 | # the extra test level it automatically adds.
|
---|
663 | #
|
---|
664 | # More benchmark heuristics: Walk down until we find more than one
|
---|
665 | # test or values.
|
---|
666 | oSub2 = oSub;
|
---|
667 | while len(oSub2.aoChildren) == 1 and not oSub2.aoValues:
|
---|
668 | oSub2 = oSub2.aoChildren[0];
|
---|
669 | if not oSub2.aoValues:
|
---|
670 | oSub2 = oSub;
|
---|
671 | self.oTest.mergeInIncludedTest(oSub2);
|
---|
672 | return True;
|
---|
673 |
|
---|
674 | def parseTestResult(sXmlFile):
|
---|
675 | """
|
---|
676 | Parses the test results in the XML.
|
---|
677 | Returns result tree.
|
---|
678 | Returns None on failure.
|
---|
679 | """
|
---|
680 | oXlr = XmlLogReader(sXmlFile);
|
---|
681 | if oXlr.parse():
|
---|
682 | if len(oXlr.oRoot.aoChildren) == 1 and not oXlr.oRoot.aoValues:
|
---|
683 | return oXlr.oRoot.aoChildren[0];
|
---|
684 | return oXlr.oRoot;
|
---|
685 | return None;
|
---|
686 |
|
---|