VirtualBox

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

Last change on this file since 97266 was 97266, checked in by vboxsync, 3 years ago

ValKit/analysis: Wrote a new analyzer/comparison tool, tst-a1.py didn't work any more and even after fixing bitrot it didn't provide very useful output.

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
File size: 24.2 KB
Line 
1# -*- coding: utf-8 -*-
2# $Id: reader.py 97266 2022-10-23 00:06:26Z 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.215389.xyz.
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: 97266 $"
43__all__ = [ 'parseTestResult', ]
44
45# Standard python imports.
46import datetime;
47import os;
48import sys;
49import traceback;
50
51# Only the main script needs to modify the path.
52try: __file__;
53except: __file__ = sys.argv[0];
54g_ksValidationKitDir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)));
55sys.path.append(g_ksValidationKitDir);
56
57# ValidationKit imports.
58from common import utils;
59
60# Python 3 hacks:
61if sys.version_info[0] >= 3:
62 long = int; # pylint: disable=redefined-builtin,invalid-name
63
64# pylint: disable=missing-docstring
65
66
67class 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
201class 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
579class 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
674def 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
Note: See TracBrowser for help on using the repository browser.

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