VirtualBox

source: vbox/trunk/src/VBox/ValidationKit/analysis/analyze.py@ 97271

Last change on this file since 97271 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 LF
  • Property svn:executable set to *
  • Property svn:keywords set to Author Date Id Revision
File size: 16.8 KB
Line 
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3# $Id: analyze.py 97267 2022-10-24 00:09:44Z vboxsync $
4
5"""
6Analyzer CLI.
7"""
8
9__copyright__ = \
10"""
11Copyright (C) 2010-2022 Oracle and/or its affiliates.
12
13This file is part of VirtualBox base platform packages, as
14available from https://www.virtualbox.org.
15
16This program is free software; you can redistribute it and/or
17modify it under the terms of the GNU General Public License
18as published by the Free Software Foundation, in version 3 of the
19License.
20
21This program is distributed in the hope that it will be useful, but
22WITHOUT ANY WARRANTY; without even the implied warranty of
23MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
24General Public License for more details.
25
26You should have received a copy of the GNU General Public License
27along with this program; if not, see <https://www.gnu.org/licenses>.
28
29The contents of this file may alternatively be used under the terms
30of the Common Development and Distribution License Version 1.0
31(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
32in the VirtualBox distribution, in which case the provisions of the
33CDDL are applicable instead of those of the GPL.
34
35You may elect to license modified versions of this file under the
36terms and conditions of either the GPL or the CDDL or both.
37
38SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
39"""
40__version__ = "$Revision: 97267 $"
41
42# Standard python imports.
43import re;
44import os;
45import textwrap;
46import sys;
47
48# Only the main script needs to modify the path.
49try: __file__
50except: __file__ = sys.argv[0];
51g_ksValidationKitDir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)));
52sys.path.append(g_ksValidationKitDir);
53
54# Validation Kit imports.
55from analysis import reader
56from analysis import reporting
57
58
59def usage():
60 """
61 Display usage.
62 """
63 # Set up the output wrapper.
64 try: cCols = os.get_terminal_size()[0] # since 3.3
65 except: cCols = 79;
66 oWrapper = textwrap.TextWrapper(width = cCols);
67
68 # Do the outputting.
69 print('Tool for comparing test results.');
70 print('');
71 oWrapper.subsequent_indent = ' ' * (len('usage: ') + 4);
72 print(oWrapper.fill('usage: analyze.py [options] [collection-1] -- [collection-2] [-- [collection3] [..]])'))
73 oWrapper.subsequent_indent = '';
74 print('');
75 print(oWrapper.fill('This tool compares two or more result collections, using one as a baseline (first by default) '
76 'and showing how the results in others differs from it.'));
77 print('');
78 print(oWrapper.fill('The results (XML file) from one or more test runs makes up a collection. A collection can be '
79 'named using the --name <name> option, or will get a sequential name automatically. The baseline '
80 'collection will have "(baseline)" appended to its name.'));
81 print('');
82 print(oWrapper.fill('A test run produces one XML file, either via the testdriver/reporter.py machinery or via the IPRT '
83 'test.cpp code. In the latter case it can be enabled and controlled via IPRT_TEST_FILE. A collection '
84 'consists of one or more of test runs (i.e. XML result files). These are combined (aka distilled) '
85 'into a single set of results before comparing them with the others. The --best and --avg options '
86 'controls how this combining is done. The need for this is mainly to try counteract some of the '
87 'instability typically found in the restuls. Just because one test run produces a better result '
88 'after a change does not necessarily mean this will always be the case and that the change was to '
89 'the better, it might just have been regular fluctuations in the test results.'));
90
91 oWrapper.initial_indent = ' ';
92 oWrapper.subsequent_indent = ' ';
93 print('');
94 print('Options governing combining (distillation):');
95 print(' --avg, --average');
96 print(oWrapper.fill('Picks the best result by calculating the average values across all the runs.'));
97 print('');
98 print(' --best');
99 print(oWrapper.fill('Picks the best result from all the runs. For values, this means making guessing what result is '
100 'better based on the unit. This may not always lead to the right choices.'));
101 print(oWrapper.initial_indent + 'Default: --best');
102
103 print('');
104 print('Options relating to collections:');
105 print(' --name <name>');
106 print(oWrapper.fill('Sets the name of the current collection. By default a collection gets a sequential number.'));
107 print('');
108 print(' --baseline <num>');
109 print(oWrapper.fill('Sets collection given by <num> (0-based) as the baseline collection.'));
110 print(oWrapper.initial_indent + 'Default: --baseline 0')
111
112 print('');
113 print('Filtering options:');
114 print(' --filter-test <substring>');
115 print(oWrapper.fill('Exclude tests not containing any of the substrings given via the --filter-test option. The '
116 'matching is done with full test name, i.e. all parent names are prepended with ", " as separator '
117 '(for example "tstIOInstr, CPUID EAX=1").'));
118 print('');
119 print(' --filter-test-out <substring>');
120 print(oWrapper.fill('Exclude tests containing the given substring. As with --filter-test, the matching is done against '
121 'the full test name.'));
122 print('');
123 print(' --filter-value <substring>');
124 print(oWrapper.fill('Exclude values not containing any of the substrings given via the --filter-value option. The '
125 'matching is done against the value name prefixed by the full test name and ": " '
126 '(for example "tstIOInstr, CPUID EAX=1: real mode, CPUID").'));
127 print('');
128 print(' --filter-value-out <substring>');
129 print(oWrapper.fill('Exclude value containing the given substring. As with --filter-value, the matching is done against '
130 'the value name prefixed by the full test name.'));
131
132 print('');
133 print(' --regex-test <expr>');
134 print(oWrapper.fill('Same as --filter-test except the substring matching is done via a regular expression.'));
135 print('');
136 print(' --regex-test-out <expr>');
137 print(oWrapper.fill('Same as --filter-test-out except the substring matching is done via a regular expression.'));
138 print('');
139 print(' --regex-value <expr>');
140 print(oWrapper.fill('Same as --filter-value except the substring matching is done via a regular expression.'));
141 print('');
142 print(' --regex-value-out <expr>');
143 print(oWrapper.fill('Same as --filter-value-out except the substring matching is done via a regular expression.'));
144 print('');
145 print(' --filter-out-empty-leaf-tests');
146 print(oWrapper.fill('Removes any leaf tests that are without any values or sub-tests. This is useful when '
147 'only considering values, especially when doing additional value filtering.'));
148
149 print('');
150 print('Output options:');
151 print(' --brief, --verbose');
152 print(oWrapper.fill('Whether to omit (--brief) the value for non-baseline runs and just get along with the difference.'));
153 print(oWrapper.initial_indent + 'Default: --brief');
154 print('');
155 print(' --pct <num>, --pct-precision <num>');
156 print(oWrapper.fill('Specifies the number of decimal place to use when formatting the difference as percent.'));
157 print(oWrapper.initial_indent + 'Default: --pct 2');
158 return 1;
159
160
161class ResultCollection(object):
162 """
163 One or more test runs that should be merged before comparison.
164 """
165
166 def __init__(self, sName):
167 self.sName = sName;
168 self.aoTestTrees = [] # type: [Test]
169 self.asTestFiles = [] # type: [str] - runs parallel to aoTestTrees
170 self.oDistilled = None # type: Test
171
172 def append(self, sFilename):
173 """
174 Loads sFilename and appends the result.
175 Returns True on success, False on failure.
176 """
177 oTestTree = reader.parseTestResult(sFilename);
178 if oTestTree:
179 self.aoTestTrees.append(oTestTree);
180 self.asTestFiles.append(sFilename);
181 return True;
182 return False;
183
184 def isEmpty(self):
185 """ Checks if the result is empty. """
186 return len(self.aoTestTrees) == 0;
187
188 def filterTests(self, asFilters):
189 """
190 Keeps all the tests in the test trees sub-string matching asFilters (str or re).
191 """
192 for oTestTree in self.aoTestTrees:
193 oTestTree.filterTests(asFilters);
194 return self;
195
196 def filterOutTests(self, asFilters):
197 """
198 Removes all the tests in the test trees sub-string matching asFilters (str or re).
199 """
200 for oTestTree in self.aoTestTrees:
201 oTestTree.filterOutTests(asFilters);
202 return self;
203
204 def filterValues(self, asFilters):
205 """
206 Keeps all the tests in the test trees sub-string matching asFilters (str or re).
207 """
208 for oTestTree in self.aoTestTrees:
209 oTestTree.filterValues(asFilters);
210 return self;
211
212 def filterOutValues(self, asFilters):
213 """
214 Removes all the tests in the test trees sub-string matching asFilters (str or re).
215 """
216 for oTestTree in self.aoTestTrees:
217 oTestTree.filterOutValues(asFilters);
218 return self;
219
220 def filterOutEmptyLeafTests(self):
221 """
222 Removes all the tests in the test trees that have neither child tests nor values.
223 """
224 for oTestTree in self.aoTestTrees:
225 oTestTree.filterOutEmptyLeafTests();
226 return self;
227
228 def distill(self, sMethod, fDropLoners = False):
229 """
230 Distills the set of test results into a single one by the given method.
231
232 Valid sMethod values:
233 - 'best': Pick the best result for each test and value among all the test runs.
234 - 'avg': Calculate the average value among all the test runs.
235
236 When fDropLoners is True, tests and values that only appear in a single test run
237 will be discarded. When False (the default), the lone result will be used.
238 """
239 assert sMethod in ['best', 'avg'];
240 assert not self.oDistilled;
241
242 # If empty, nothing to do.
243 if self.isEmpty():
244 return None;
245
246 # If there is only a single tree, make a deep copy of it.
247 if len(self.aoTestTrees) == 1:
248 oDistilled = self.aoTestTrees[0].clone();
249 else:
250
251 # Since we don't know if the test runs are all from the same test, we create
252 # dummy root tests for each run and use these are the start for the distillation.
253 aoDummyInputTests = [];
254 for oRun in self.aoTestTrees:
255 oDummy = reader.Test();
256 oDummy.aoChildren = [oRun,];
257 aoDummyInputTests.append(oDummy);
258
259 # Similarly, we end up with a "dummy" root test for the result.
260 oDistilled = reader.Test();
261 oDistilled.distill(aoDummyInputTests, sMethod, fDropLoners);
262
263 # We can drop this if there is only a single child, i.e. if all runs are for
264 # the same test.
265 if len(oDistilled.aoChildren) == 1:
266 oDistilled = oDistilled.aoChildren[0];
267
268 self.oDistilled = oDistilled;
269 return oDistilled;
270
271
272
273# matchWithValue hacks.
274g_asOptions = [];
275g_iOptInd = 1;
276g_sOptArg = '';
277
278def matchWithValue(sOption):
279 """ Matches an option with a value, placing the value in g_sOptArg if it matches. """
280 global g_asOptions, g_iOptInd, g_sOptArg;
281 sArg = g_asOptions[g_iOptInd];
282 if sArg.startswith(sOption):
283 if len(sArg) == len(sOption):
284 if g_iOptInd + 1 < len(g_asOptions):
285 g_iOptInd += 1;
286 g_sOptArg = g_asOptions[g_iOptInd];
287 return True;
288
289 print('syntax error: Option %s takes a value!' % (sOption,));
290 raise Exception('syntax error: Option %s takes a value!' % (sOption,));
291
292 if sArg[len(sOption)] in ('=', ':'):
293 g_sOptArg = sArg[len(sOption) + 1:];
294 return True;
295 return False;
296
297
298def main(asArgs):
299 """ C style main(). """
300 #
301 # Parse arguments
302 #
303 oCurCollection = ResultCollection('#0');
304 aoCollections = [ oCurCollection, ];
305 iBaseline = 0;
306 sDistillationMethod = 'best';
307 fBrief = True;
308 cPctPrecision = 2;
309 asTestFilters = [];
310 asTestOutFilters = [];
311 asValueFilters = [];
312 asValueOutFilters = [];
313 fFilterOutEmptyLeafTest = True;
314
315 global g_asOptions, g_iOptInd, g_sOptArg;
316 g_asOptions = asArgs;
317 g_iOptInd = 1;
318 while g_iOptInd < len(asArgs):
319 sArg = asArgs[g_iOptInd];
320 g_sOptArg = '';
321 #print("dbg: g_iOptInd=%s '%s'" % (g_iOptInd, sArg,));
322
323 if sArg.startswith('--help'):
324 return usage();
325
326 if matchWithValue('--filter-test'):
327 asTestFilters.append(g_sOptArg);
328 elif matchWithValue('--filter-test-out'):
329 asTestOutFilters.append(g_sOptArg);
330 elif matchWithValue('--filter-value'):
331 asValueFilters.append(g_sOptArg);
332 elif matchWithValue('--filter-value-out'):
333 asValueOutFilters.append(g_sOptArg);
334
335 elif matchWithValue('--regex-test'):
336 asTestFilters.append(re.compile(g_sOptArg));
337 elif matchWithValue('--regex-test-out'):
338 asTestOutFilters.append(re.compile(g_sOptArg));
339 elif matchWithValue('--regex-value'):
340 asValueFilters.append(re.compile(g_sOptArg));
341 elif matchWithValue('--regex-value-out'):
342 asValueOutFilters.append(re.compile(g_sOptArg));
343
344 elif sArg == '--filter-out-empty-leaf-tests':
345 fFilterOutEmptyLeafTest = True;
346 elif sArg == '--no-filter-out-empty-leaf-tests':
347 fFilterOutEmptyLeafTest = False;
348
349 elif sArg == '--best':
350 sDistillationMethod = 'best';
351 elif sArg in ('--avg', '--average'):
352 sDistillationMethod = 'avg';
353
354 elif sArg == '--brief':
355 fBrief = True;
356 elif sArg == '--verbose':
357 fBrief = False;
358
359 elif matchWithValue('--pct') or matchWithValue('--pct-precision'):
360 cPctPrecision = int(g_sOptArg);
361 elif matchWithValue('--base') or matchWithValue('--baseline'):
362 iBaseline = int(g_sOptArg);
363
364 # '--' starts a new collection. If current one is empty, drop it.
365 elif sArg == '--':
366 print("dbg: new collection");
367 #if oCurCollection.isEmpty():
368 # del aoCollections[-1];
369 oCurCollection = ResultCollection("#%s" % (len(aoCollections),));
370 aoCollections.append(oCurCollection);
371
372 # Name the current result collection.
373 elif matchWithValue('--name'):
374 oCurCollection.sName = g_sOptArg;
375
376 # Read in a file and add it to the current data set.
377 else:
378 if not oCurCollection.append(sArg):
379 return 1;
380 g_iOptInd += 1;
381
382 #
383 # Post argument parsing processing.
384 #
385
386 # Drop the last collection if empty.
387 if oCurCollection.isEmpty():
388 del aoCollections[-1];
389 if not aoCollections:
390 print("error: No input files given!");
391 return 1;
392
393 # Check the baseline value and mark the column as such.
394 if iBaseline < 0 or iBaseline > len(aoCollections):
395 print("error: specified baseline is out of range: %s, valid range 0 <= baseline < %s"
396 % (iBaseline, len(aoCollections),));
397 return 1;
398 aoCollections[iBaseline].sName += ' (baseline)';
399
400 #
401 # Apply filtering before distilling each collection into a single result tree.
402 #
403 if asTestFilters:
404 for oCollection in aoCollections:
405 oCollection.filterTests(asTestFilters);
406 if asTestOutFilters:
407 for oCollection in aoCollections:
408 oCollection.filterOutTests(asTestOutFilters);
409
410 if asValueFilters:
411 for oCollection in aoCollections:
412 oCollection.filterValues(asValueFilters);
413 if asValueOutFilters:
414 for oCollection in aoCollections:
415 oCollection.filterOutValues(asValueOutFilters);
416
417 if fFilterOutEmptyLeafTest:
418 for oCollection in aoCollections:
419 oCollection.filterOutEmptyLeafTests();
420
421 # Distillation.
422 for oCollection in aoCollections:
423 oCollection.distill(sDistillationMethod);
424
425 #
426 # Produce the report.
427 #
428 oTable = reporting.RunTable(iBaseline, fBrief, cPctPrecision);
429 oTable.populateFromRuns([oCollection.oDistilled for oCollection in aoCollections],
430 [oCollection.sName for oCollection in aoCollections]);
431 print('\n'.join(oTable.formatAsText()));
432 return 0;
433
434if __name__ == '__main__':
435 sys.exit(main(sys.argv));
436
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