VirtualBox

source: vbox/trunk/src/VBox/ValidationKit/analysis/reader.py@ 97267

Last change on this file since 97267 was 97267, checked in by vboxsync, 2 years ago

ValKit/analysis: Adding better filtering and some documentation (--help) for the analyze tool. Some improvements for --option[:=]value parsing too.

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