1 | # -*- coding: utf-8 -*-
|
---|
2 | # $Id: wuigraphwiz.py 106061 2024-09-16 14:03:52Z vboxsync $
|
---|
3 |
|
---|
4 | """
|
---|
5 | Test Manager WUI - Graph Wizard
|
---|
6 | """
|
---|
7 |
|
---|
8 | __copyright__ = \
|
---|
9 | """
|
---|
10 | Copyright (C) 2012-2024 Oracle and/or its affiliates.
|
---|
11 |
|
---|
12 | This file is part of VirtualBox base platform packages, as
|
---|
13 | available from https://www.virtualbox.org.
|
---|
14 |
|
---|
15 | This program is free software; you can redistribute it and/or
|
---|
16 | modify it under the terms of the GNU General Public License
|
---|
17 | as published by the Free Software Foundation, in version 3 of the
|
---|
18 | License.
|
---|
19 |
|
---|
20 | This program is distributed in the hope that it will be useful, but
|
---|
21 | WITHOUT ANY WARRANTY; without even the implied warranty of
|
---|
22 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
---|
23 | General Public License for more details.
|
---|
24 |
|
---|
25 | You should have received a copy of the GNU General Public License
|
---|
26 | along with this program; if not, see <https://www.gnu.org/licenses>.
|
---|
27 |
|
---|
28 | The contents of this file may alternatively be used under the terms
|
---|
29 | of the Common Development and Distribution License Version 1.0
|
---|
30 | (CDDL), a copy of it is provided in the "COPYING.CDDL" file included
|
---|
31 | in the VirtualBox distribution, in which case the provisions of the
|
---|
32 | CDDL are applicable instead of those of the GPL.
|
---|
33 |
|
---|
34 | You may elect to license modified versions of this file under the
|
---|
35 | terms and conditions of either the GPL or the CDDL or both.
|
---|
36 |
|
---|
37 | SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
|
---|
38 | """
|
---|
39 | __version__ = "$Revision: 106061 $"
|
---|
40 |
|
---|
41 | # Python imports.
|
---|
42 | import functools;
|
---|
43 |
|
---|
44 | # Validation Kit imports.
|
---|
45 | from testmanager.webui.wuimain import WuiMain;
|
---|
46 | from testmanager.webui.wuihlpgraph import WuiHlpLineGraphErrorbarY, WuiHlpGraphDataTableEx;
|
---|
47 | from testmanager.webui.wuireport import WuiReportBase;
|
---|
48 |
|
---|
49 | from common import utils, webutils;
|
---|
50 | from common import constants;
|
---|
51 |
|
---|
52 |
|
---|
53 | class WuiGraphWiz(WuiReportBase):
|
---|
54 | """Construct a graph for analyzing test results (values) across builds and testboxes."""
|
---|
55 |
|
---|
56 | ## @name Series name parts.
|
---|
57 | ## @{
|
---|
58 | kfSeriesName_TestBox = 1;
|
---|
59 | kfSeriesName_Product = 2;
|
---|
60 | kfSeriesName_Branch = 4;
|
---|
61 | kfSeriesName_BuildType = 8;
|
---|
62 | kfSeriesName_OsArchs = 16;
|
---|
63 | kfSeriesName_TestCase = 32;
|
---|
64 | kfSeriesName_TestCaseArgs = 64;
|
---|
65 | kfSeriesName_All = 127;
|
---|
66 | ## @}
|
---|
67 |
|
---|
68 |
|
---|
69 | def __init__(self, oModel, dParams, fSubReport = False, fnDPrint = None, oDisp = None):
|
---|
70 | WuiReportBase.__init__(self, oModel, dParams, fSubReport = fSubReport, fnDPrint = fnDPrint, oDisp = oDisp);
|
---|
71 |
|
---|
72 | # Select graph implementation.
|
---|
73 | if dParams[WuiMain.ksParamGraphWizImpl] == 'charts':
|
---|
74 | from testmanager.webui.wuihlpgraphgooglechart import WuiHlpLineGraphErrorbarY as MyGraph;
|
---|
75 | self.oGraphClass = MyGraph;
|
---|
76 | elif dParams[WuiMain.ksParamGraphWizImpl] == 'matplotlib':
|
---|
77 | from testmanager.webui.wuihlpgraphmatplotlib import WuiHlpLineGraphErrorbarY as MyGraph;
|
---|
78 | self.oGraphClass = MyGraph;
|
---|
79 | else:
|
---|
80 | self.oGraphClass = WuiHlpLineGraphErrorbarY;
|
---|
81 |
|
---|
82 |
|
---|
83 | #
|
---|
84 | def _figureSeriesNameBits(self, aoSeries):
|
---|
85 | """ Figures out the method (bitmask) to use when naming series. """
|
---|
86 | if len(aoSeries) <= 1:
|
---|
87 | return WuiGraphWiz.kfSeriesName_TestBox;
|
---|
88 |
|
---|
89 | # Start with all and drop unnecessary specs one-by-one.
|
---|
90 | fRet = WuiGraphWiz.kfSeriesName_All;
|
---|
91 |
|
---|
92 | if [oSrs.idTestBox for oSrs in aoSeries].count(aoSeries[0].idTestBox) == len(aoSeries):
|
---|
93 | fRet &= ~WuiGraphWiz.kfSeriesName_TestBox;
|
---|
94 |
|
---|
95 | if [oSrs.idBuildCategory for oSrs in aoSeries].count(aoSeries[0].idBuildCategory) == len(aoSeries):
|
---|
96 | fRet &= ~WuiGraphWiz.kfSeriesName_Product;
|
---|
97 | fRet &= ~WuiGraphWiz.kfSeriesName_Branch;
|
---|
98 | fRet &= ~WuiGraphWiz.kfSeriesName_BuildType;
|
---|
99 | fRet &= ~WuiGraphWiz.kfSeriesName_OsArchs;
|
---|
100 | else:
|
---|
101 | if [oSrs.oBuildCategory.sProduct for oSrs in aoSeries].count(aoSeries[0].oBuildCategory.sProduct) == len(aoSeries):
|
---|
102 | fRet &= ~WuiGraphWiz.kfSeriesName_Product;
|
---|
103 | if [oSrs.oBuildCategory.sBranch for oSrs in aoSeries].count(aoSeries[0].oBuildCategory.sBranch) == len(aoSeries):
|
---|
104 | fRet &= ~WuiGraphWiz.kfSeriesName_Branch;
|
---|
105 | if [oSrs.oBuildCategory.sType for oSrs in aoSeries].count(aoSeries[0].oBuildCategory.sType) == len(aoSeries):
|
---|
106 | fRet &= ~WuiGraphWiz.kfSeriesName_BuildType;
|
---|
107 |
|
---|
108 | # Complicated.
|
---|
109 | fRet &= ~WuiGraphWiz.kfSeriesName_OsArchs;
|
---|
110 | daTestBoxes = {};
|
---|
111 | for oSeries in aoSeries:
|
---|
112 | if oSeries.idTestBox in daTestBoxes:
|
---|
113 | daTestBoxes[oSeries.idTestBox].append(oSeries);
|
---|
114 | else:
|
---|
115 | daTestBoxes[oSeries.idTestBox] = [oSeries,];
|
---|
116 | for aoSeriesPerTestBox in daTestBoxes.values():
|
---|
117 | if len(aoSeriesPerTestBox) >= 0:
|
---|
118 | asOsArches = aoSeriesPerTestBox[0].oBuildCategory.asOsArches;
|
---|
119 | for i in range(1, len(aoSeriesPerTestBox)):
|
---|
120 | if aoSeriesPerTestBox[i].oBuildCategory.asOsArches != asOsArches:
|
---|
121 | fRet |= WuiGraphWiz.kfSeriesName_OsArchs;
|
---|
122 | break;
|
---|
123 |
|
---|
124 | if aoSeries[0].oTestCaseArgs is None:
|
---|
125 | fRet &= ~WuiGraphWiz.kfSeriesName_TestCaseArgs;
|
---|
126 | if [oSrs.idTestCase for oSrs in aoSeries].count(aoSeries[0].idTestCase) == len(aoSeries):
|
---|
127 | fRet &= ~WuiGraphWiz.kfSeriesName_TestCase;
|
---|
128 | else:
|
---|
129 | fRet &= ~WuiGraphWiz.kfSeriesName_TestCase;
|
---|
130 | if [oSrs.idTestCaseArgs for oSrs in aoSeries].count(aoSeries[0].idTestCaseArgs) == len(aoSeries):
|
---|
131 | fRet &= ~WuiGraphWiz.kfSeriesName_TestCaseArgs;
|
---|
132 |
|
---|
133 | return fRet;
|
---|
134 |
|
---|
135 | def _getSeriesNameFromBits(self, oSeries, fBits):
|
---|
136 | """ Creates a series name from bits (kfSeriesName_xxx). """
|
---|
137 | assert fBits != 0;
|
---|
138 | sName = '';
|
---|
139 |
|
---|
140 | if fBits & WuiGraphWiz.kfSeriesName_Product:
|
---|
141 | if sName: sName += ' / ';
|
---|
142 | sName += oSeries.oBuildCategory.sProduct;
|
---|
143 |
|
---|
144 | if fBits & WuiGraphWiz.kfSeriesName_Branch:
|
---|
145 | if sName: sName += ' / ';
|
---|
146 | sName += oSeries.oBuildCategory.sBranch;
|
---|
147 |
|
---|
148 | if fBits & WuiGraphWiz.kfSeriesName_BuildType:
|
---|
149 | if sName: sName += ' / ';
|
---|
150 | sName += oSeries.oBuildCategory.sType;
|
---|
151 |
|
---|
152 | if fBits & WuiGraphWiz.kfSeriesName_OsArchs:
|
---|
153 | if sName: sName += ' / ';
|
---|
154 | sName += ' & '.join(oSeries.oBuildCategory.asOsArches);
|
---|
155 |
|
---|
156 | if fBits & WuiGraphWiz.kfSeriesName_TestCaseArgs:
|
---|
157 | if sName: sName += ' / ';
|
---|
158 | if oSeries.idTestCaseArgs is not None:
|
---|
159 | sName += oSeries.oTestCase.sName + ':#' + str(oSeries.idTestCaseArgs);
|
---|
160 | else:
|
---|
161 | sName += oSeries.oTestCase.sName;
|
---|
162 | elif fBits & WuiGraphWiz.kfSeriesName_TestCase:
|
---|
163 | if sName: sName += ' / ';
|
---|
164 | sName += oSeries.oTestCase.sName;
|
---|
165 |
|
---|
166 | if fBits & WuiGraphWiz.kfSeriesName_TestBox:
|
---|
167 | if sName: sName += ' / ';
|
---|
168 | sName += oSeries.oTestBox.sName;
|
---|
169 |
|
---|
170 | return sName;
|
---|
171 |
|
---|
172 | def _calcGraphName(self, oSeries, fSeriesName, sSampleName):
|
---|
173 | """ Constructs a name for the graph. """
|
---|
174 | fGraphName = ~fSeriesName & ( WuiGraphWiz.kfSeriesName_TestBox
|
---|
175 | | WuiGraphWiz.kfSeriesName_Product
|
---|
176 | | WuiGraphWiz.kfSeriesName_Branch
|
---|
177 | | WuiGraphWiz.kfSeriesName_BuildType
|
---|
178 | );
|
---|
179 | sName = self._getSeriesNameFromBits(oSeries, fGraphName);
|
---|
180 | if sName: sName += ' - ';
|
---|
181 | sName += sSampleName;
|
---|
182 | return sName;
|
---|
183 |
|
---|
184 | def _calcSampleName(self, oCollection):
|
---|
185 | """ Constructs a name for a sample source (collection). """
|
---|
186 | if oCollection.sValue is not None:
|
---|
187 | asSampleName = [oCollection.sValue, 'in',];
|
---|
188 | elif oCollection.sType == self._oModel.ksTypeElapsed:
|
---|
189 | asSampleName = ['Elapsed time', 'for', ];
|
---|
190 | elif oCollection.sType == self._oModel.ksTypeResult:
|
---|
191 | asSampleName = ['Error count', 'for',];
|
---|
192 | else:
|
---|
193 | return 'Invalid collection type: "%s"' % (oCollection.sType,);
|
---|
194 |
|
---|
195 | sTestName = ', '.join(oCollection.asTests if oCollection.asTests[0] else oCollection.asTests[1:]);
|
---|
196 | if sTestName == '':
|
---|
197 | # Use the testcase name if there is only one for all series.
|
---|
198 | if not oCollection.aoSeries:
|
---|
199 | return asSampleName[0];
|
---|
200 | if len(oCollection.aoSeries) > 1:
|
---|
201 | idTestCase = oCollection.aoSeries[0].idTestCase;
|
---|
202 | for oSeries in oCollection.aoSeries:
|
---|
203 | if oSeries.idTestCase != idTestCase:
|
---|
204 | return asSampleName[0];
|
---|
205 | sTestName = oCollection.aoSeries[0].oTestCase.sName;
|
---|
206 | return ' '.join(asSampleName) + ' ' + sTestName;
|
---|
207 |
|
---|
208 |
|
---|
209 | def _splitSeries(self, aoSeries):
|
---|
210 | """
|
---|
211 | Splits the data series (ReportGraphModel.DataSeries) into one or more graphs.
|
---|
212 |
|
---|
213 | Returns an array of data series arrays.
|
---|
214 | """
|
---|
215 | # Must be at least two series for something to be splittable.
|
---|
216 | if len(aoSeries) <= 1:
|
---|
217 | if not aoSeries:
|
---|
218 | return [];
|
---|
219 | return [aoSeries,];
|
---|
220 |
|
---|
221 | # Split on unit.
|
---|
222 | dUnitSeries = {};
|
---|
223 | for oSeries in aoSeries:
|
---|
224 | if oSeries.iUnit not in dUnitSeries:
|
---|
225 | dUnitSeries[oSeries.iUnit] = [];
|
---|
226 | dUnitSeries[oSeries.iUnit].append(oSeries);
|
---|
227 |
|
---|
228 | # Sort the per-unit series since the build category was only sorted by ID.
|
---|
229 | for iUnit in dUnitSeries:
|
---|
230 | def mycmp(oSelf, oOther):
|
---|
231 | """ __cmp__ like function. """
|
---|
232 | iCmp = utils.stricmp(oSelf.oBuildCategory.sProduct, oOther.oBuildCategory.sProduct);
|
---|
233 | if iCmp != 0:
|
---|
234 | return iCmp;
|
---|
235 | iCmp = utils.stricmp(oSelf.oBuildCategory.sBranch, oOther.oBuildCategory.sBranch);
|
---|
236 | if iCmp != 0:
|
---|
237 | return iCmp;
|
---|
238 | iCmp = utils.stricmp(oSelf.oBuildCategory.sType, oOther.oBuildCategory.sType);
|
---|
239 | if iCmp != 0:
|
---|
240 | return iCmp;
|
---|
241 | iCmp = utils.stricmp(oSelf.oTestBox.sName, oOther.oTestBox.sName);
|
---|
242 | if iCmp != 0:
|
---|
243 | return iCmp;
|
---|
244 | return 0;
|
---|
245 | dUnitSeries[iUnit] = sorted(dUnitSeries[iUnit], key = functools.cmp_to_key(mycmp));
|
---|
246 |
|
---|
247 | # Split the per-unit series up if necessary.
|
---|
248 | cMaxPerGraph = self._dParams[WuiMain.ksParamGraphWizMaxPerGraph];
|
---|
249 | aaoRet = [];
|
---|
250 | for aoUnitSeries in dUnitSeries.values():
|
---|
251 | while len(aoUnitSeries) > cMaxPerGraph:
|
---|
252 | aaoRet.append(aoUnitSeries[:cMaxPerGraph]);
|
---|
253 | aoUnitSeries = aoUnitSeries[cMaxPerGraph:];
|
---|
254 | if aoUnitSeries:
|
---|
255 | aaoRet.append(aoUnitSeries);
|
---|
256 |
|
---|
257 | return aaoRet;
|
---|
258 |
|
---|
259 | def _configureGraph(self, oGraph):
|
---|
260 | """
|
---|
261 | Configures oGraph according to user parameters and other config settings.
|
---|
262 |
|
---|
263 | Returns oGraph.
|
---|
264 | """
|
---|
265 | oGraph.setWidth(self._dParams[WuiMain.ksParamGraphWizWidth])
|
---|
266 | oGraph.setHeight(self._dParams[WuiMain.ksParamGraphWizHeight])
|
---|
267 | oGraph.setDpi(self._dParams[WuiMain.ksParamGraphWizDpi])
|
---|
268 | oGraph.setErrorBarY(self._dParams[WuiMain.ksParamGraphWizErrorBarY]);
|
---|
269 | oGraph.setFontSize(self._dParams[WuiMain.ksParamGraphWizFontSize]);
|
---|
270 | if hasattr(oGraph, 'setXkcdStyle'):
|
---|
271 | oGraph.setXkcdStyle(self._dParams[WuiMain.ksParamGraphWizXkcdStyle]);
|
---|
272 |
|
---|
273 | return oGraph;
|
---|
274 |
|
---|
275 | def _generateInteractiveForm(self):
|
---|
276 | """
|
---|
277 | Generates the HTML for the interactive form.
|
---|
278 | Returns (sTopOfForm, sEndOfForm)
|
---|
279 | """
|
---|
280 |
|
---|
281 | #
|
---|
282 | # The top of the form.
|
---|
283 | #
|
---|
284 | sTop = '<form action="#" method="get" id="graphwiz-form">\n' \
|
---|
285 | ' <input type="hidden" name="%s" value="%s"/>\n' \
|
---|
286 | ' <input type="hidden" name="%s" value="%u"/>\n' \
|
---|
287 | % ( WuiMain.ksParamAction, WuiMain.ksActionGraphWiz,
|
---|
288 | WuiMain.ksParamGraphWizSrcTestSetId, self._dParams[WuiMain.ksParamGraphWizSrcTestSetId],
|
---|
289 | );
|
---|
290 |
|
---|
291 | sTop += ' <div id="graphwiz-nav">\n';
|
---|
292 | sTop += ' <script type="text/javascript">\n' \
|
---|
293 | ' window.onresize = function(){ return graphwizOnResizeRecalcWidth("graphwiz-nav", "%s"); }\n' \
|
---|
294 | ' window.onload = function(){ return graphwizOnLoadRememberWidth("graphwiz-nav"); }\n' \
|
---|
295 | ' </script>\n' \
|
---|
296 | % ( WuiMain.ksParamGraphWizWidth, );
|
---|
297 |
|
---|
298 | #
|
---|
299 | # Top: First row.
|
---|
300 | #
|
---|
301 | sTop += ' <div id="graphwiz-top-1">\n';
|
---|
302 |
|
---|
303 | # time.
|
---|
304 | sNow = self._dParams[WuiMain.ksParamEffectiveDate];
|
---|
305 | if sNow is None: sNow = '';
|
---|
306 | sTop += ' <div id="graphwiz-time">\n';
|
---|
307 | sTop += ' <label for="%s">Starting:</label>\n' \
|
---|
308 | ' <input type="text" name="%s" id="%s" value="%s" class="graphwiz-time-input"/>\n' \
|
---|
309 | % ( WuiMain.ksParamEffectiveDate,
|
---|
310 | WuiMain.ksParamEffectiveDate, WuiMain.ksParamEffectiveDate, sNow, );
|
---|
311 |
|
---|
312 | sTop += ' <input type="hidden" name="%s" value="%u"/>\n' % ( WuiMain.ksParamReportPeriods, 1, );
|
---|
313 | sTop += ' <label for="%s"> Going back:\n' \
|
---|
314 | ' <input type="text" name="%s" id="%s" value="%s" class="graphwiz-period-input"/>\n' \
|
---|
315 | % ( WuiMain.ksParamReportPeriodInHours,
|
---|
316 | WuiMain.ksParamReportPeriodInHours, WuiMain.ksParamReportPeriodInHours,
|
---|
317 | utils.formatIntervalHours(self._dParams[WuiMain.ksParamReportPeriodInHours]) );
|
---|
318 | sTop += ' </div>\n';
|
---|
319 |
|
---|
320 | # Graph options top row.
|
---|
321 | sTop += ' <div id="graphwiz-top-options-1">\n';
|
---|
322 |
|
---|
323 | # graph type.
|
---|
324 | sTop += ' <label for="%s">Graph:</label>\n' \
|
---|
325 | ' <select name="%s" id="%s">\n' \
|
---|
326 | % ( WuiMain.ksParamGraphWizImpl, WuiMain.ksParamGraphWizImpl, WuiMain.ksParamGraphWizImpl, );
|
---|
327 | for (sImpl, sDesc) in WuiMain.kaasGraphWizImplCombo:
|
---|
328 | sTop += ' <option value="%s"%s>%s</option>\n' \
|
---|
329 | % (sImpl, ' selected' if sImpl == self._dParams[WuiMain.ksParamGraphWizImpl] else '', sDesc);
|
---|
330 | sTop += ' </select>\n';
|
---|
331 |
|
---|
332 | # graph size.
|
---|
333 | sTop += ' <label for="%s">Graph size:</label>\n' \
|
---|
334 | ' <input type="text" name="%s" id="%s" value="%s" class="graphwiz-pixel-input"> x\n' \
|
---|
335 | ' <input type="text" name="%s" id="%s" value="%s" class="graphwiz-pixel-input">\n' \
|
---|
336 | ' <label for="%s">Dpi:</label>'\
|
---|
337 | ' <input type="text" name="%s" id="%s" value="%s" class="graphwiz-dpi-input">\n' \
|
---|
338 | ' <button type="button" onclick="%s">Defaults</button>\n' \
|
---|
339 | % ( WuiMain.ksParamGraphWizWidth,
|
---|
340 | WuiMain.ksParamGraphWizWidth, WuiMain.ksParamGraphWizWidth, self._dParams[WuiMain.ksParamGraphWizWidth],
|
---|
341 | WuiMain.ksParamGraphWizHeight, WuiMain.ksParamGraphWizHeight, self._dParams[WuiMain.ksParamGraphWizHeight],
|
---|
342 | WuiMain.ksParamGraphWizDpi,
|
---|
343 | WuiMain.ksParamGraphWizDpi, WuiMain.ksParamGraphWizDpi, self._dParams[WuiMain.ksParamGraphWizDpi],
|
---|
344 | webutils.escapeAttr('return graphwizSetDefaultSizeValues("graphwiz-nav", "%s", "%s", "%s");'
|
---|
345 | % ( WuiMain.ksParamGraphWizWidth, WuiMain.ksParamGraphWizHeight,
|
---|
346 | WuiMain.ksParamGraphWizDpi )),
|
---|
347 | );
|
---|
348 |
|
---|
349 | sTop += ' </div>\n'; # (options row 1)
|
---|
350 |
|
---|
351 | sTop += ' </div>\n'; # (end of row 1)
|
---|
352 |
|
---|
353 | #
|
---|
354 | # Top: Second row.
|
---|
355 | #
|
---|
356 | sTop += ' <div id="graphwiz-top-2">\n';
|
---|
357 |
|
---|
358 | # Submit
|
---|
359 | sFormButton = '<button type="submit">Refresh</button>\n';
|
---|
360 | sTop += ' <div id="graphwiz-top-submit">' + sFormButton + '</div>\n';
|
---|
361 |
|
---|
362 |
|
---|
363 | # Options.
|
---|
364 | sTop += ' <div id="graphwiz-top-options-2">\n';
|
---|
365 |
|
---|
366 | sTop += ' <input type="checkbox" name="%s" id="%s" value="1"%s/>\n' \
|
---|
367 | ' <label for="%s">Tabular data</label>\n' \
|
---|
368 | % ( WuiMain.ksParamGraphWizTabular, WuiMain.ksParamGraphWizTabular,
|
---|
369 | ' checked' if self._dParams[WuiMain.ksParamGraphWizTabular] else '',
|
---|
370 | WuiMain.ksParamGraphWizTabular);
|
---|
371 |
|
---|
372 | if hasattr(self.oGraphClass, 'setXkcdStyle'):
|
---|
373 | sTop += ' <input type="checkbox" name="%s" id="%s" value="1"%s/>\n' \
|
---|
374 | ' <label for="%s">xkcd-style</label>\n' \
|
---|
375 | % ( WuiMain.ksParamGraphWizXkcdStyle, WuiMain.ksParamGraphWizXkcdStyle,
|
---|
376 | ' checked' if self._dParams[WuiMain.ksParamGraphWizXkcdStyle] else '',
|
---|
377 | WuiMain.ksParamGraphWizXkcdStyle);
|
---|
378 | elif self._dParams[WuiMain.ksParamGraphWizXkcdStyle]:
|
---|
379 | sTop += ' <input type="hidden" name="%s" id="%s" value="1"/>\n' \
|
---|
380 | % ( WuiMain.ksParamGraphWizXkcdStyle, WuiMain.ksParamGraphWizXkcdStyle, );
|
---|
381 |
|
---|
382 | if not hasattr(self.oGraphClass, 'kfNoErrorBarsSupport'):
|
---|
383 | sTop += ' <input type="checkbox" name="%s" id="%s" value="1"%s title="%s"/>\n' \
|
---|
384 | ' <label for="%s">Error bars,</label>\n' \
|
---|
385 | ' <label for="%s">max: </label>\n' \
|
---|
386 | ' <input type="text" name="%s" id="%s" value="%s" class="graphwiz-maxerrorbar-input" title="%s"/>\n' \
|
---|
387 | % ( WuiMain.ksParamGraphWizErrorBarY, WuiMain.ksParamGraphWizErrorBarY,
|
---|
388 | ' checked' if self._dParams[WuiMain.ksParamGraphWizErrorBarY] else '',
|
---|
389 | 'Error bars shows some of the max and min results on the Y-axis.',
|
---|
390 | WuiMain.ksParamGraphWizErrorBarY,
|
---|
391 | WuiMain.ksParamGraphWizMaxErrorBarY,
|
---|
392 | WuiMain.ksParamGraphWizMaxErrorBarY, WuiMain.ksParamGraphWizMaxErrorBarY,
|
---|
393 | self._dParams[WuiMain.ksParamGraphWizMaxErrorBarY],
|
---|
394 | 'Maximum number of Y-axis error bar per graph. (Too many makes it unreadable.)'
|
---|
395 | );
|
---|
396 | else:
|
---|
397 | if self._dParams[WuiMain.ksParamGraphWizErrorBarY]:
|
---|
398 | sTop += '<input type="hidden" name="%s" id="%s" value="1">\n' \
|
---|
399 | % ( WuiMain.ksParamGraphWizErrorBarY, WuiMain.ksParamGraphWizErrorBarY, );
|
---|
400 | sTop += '<input type="hidden" name="%s" id="%s" value="%u">\n' \
|
---|
401 | % ( WuiMain.ksParamGraphWizMaxErrorBarY, WuiMain.ksParamGraphWizMaxErrorBarY,
|
---|
402 | self._dParams[WuiMain.ksParamGraphWizMaxErrorBarY], );
|
---|
403 |
|
---|
404 | sTop += ' <label for="%s">Font size: </label>\n' \
|
---|
405 | ' <input type="text" name="%s" id="%s" value="%s" class="graphwiz-fontsize-input"/>\n' \
|
---|
406 | % ( WuiMain.ksParamGraphWizFontSize,
|
---|
407 | WuiMain.ksParamGraphWizFontSize, WuiMain.ksParamGraphWizFontSize,
|
---|
408 | self._dParams[WuiMain.ksParamGraphWizFontSize], );
|
---|
409 |
|
---|
410 | sTop += ' <label for="%s">Data series: </label>\n' \
|
---|
411 | ' <input type="text" name="%s" id="%s" value="%s" class="graphwiz-maxpergraph-input" title="%s"/>\n' \
|
---|
412 | % ( WuiMain.ksParamGraphWizMaxPerGraph,
|
---|
413 | WuiMain.ksParamGraphWizMaxPerGraph, WuiMain.ksParamGraphWizMaxPerGraph,
|
---|
414 | self._dParams[WuiMain.ksParamGraphWizMaxPerGraph],
|
---|
415 | 'Max data series per graph.' );
|
---|
416 |
|
---|
417 | sTop += ' </div>\n'; # (options row 2)
|
---|
418 |
|
---|
419 | sTop += ' </div>\n'; # (end of row 2)
|
---|
420 |
|
---|
421 | sTop += ' </div>\n'; # end of top.
|
---|
422 |
|
---|
423 | #
|
---|
424 | # The end of the page selection.
|
---|
425 | #
|
---|
426 | sEnd = ' <div id="graphwiz-end-selection">\n';
|
---|
427 |
|
---|
428 | #
|
---|
429 | # Testbox selection
|
---|
430 | #
|
---|
431 | aidTestBoxes = list(self._dParams[WuiMain.ksParamGraphWizTestBoxIds]);
|
---|
432 | sEnd += ' <div id="graphwiz-testboxes" class="graphwiz-end-selection-group">\n' \
|
---|
433 | ' <h3>TestBox Selection:</h3>\n' \
|
---|
434 | ' <ol class="tmgraph-testboxes">\n';
|
---|
435 |
|
---|
436 | # Get a list of eligible testboxes from the DB.
|
---|
437 | for oTestBox in self._oModel.getEligibleTestBoxes():
|
---|
438 | try: aidTestBoxes.remove(oTestBox.idTestBox);
|
---|
439 | except: sChecked = '';
|
---|
440 | else: sChecked = ' checked';
|
---|
441 | sEnd += ' <li><input type="checkbox" name="%s" value="%s" id="gw-tb-%u"%s/>' \
|
---|
442 | '<label for="gw-tb-%u">%s</label></li>\n' \
|
---|
443 | % ( WuiMain.ksParamGraphWizTestBoxIds, oTestBox.idTestBox, oTestBox.idTestBox, sChecked,
|
---|
444 | oTestBox.idTestBox, oTestBox.sName);
|
---|
445 |
|
---|
446 | # List testboxes that have been checked in a different period or something.
|
---|
447 | for idTestBox in aidTestBoxes:
|
---|
448 | oTestBox = self._oModel.oCache.getTestBox(idTestBox);
|
---|
449 | sEnd += ' <li><input type="checkbox" name="%s" value="%s" id="gw-tb-%u" checked/>' \
|
---|
450 | '<label for="gw-tb-%u">%s</label></li>\n' \
|
---|
451 | % ( WuiMain.ksParamGraphWizTestBoxIds, oTestBox.idTestBox, oTestBox.idTestBox,
|
---|
452 | oTestBox.idTestBox, oTestBox.sName);
|
---|
453 |
|
---|
454 | sEnd += ' </ol>\n' \
|
---|
455 | ' </div>\n';
|
---|
456 |
|
---|
457 | #
|
---|
458 | # Build category selection.
|
---|
459 | #
|
---|
460 | aidBuildCategories = list(self._dParams[WuiMain.ksParamGraphWizBuildCatIds]);
|
---|
461 | sEnd += ' <div id="graphwiz-buildcategories" class="graphwiz-end-selection-group">\n' \
|
---|
462 | ' <h3>Build Category Selection:</h3>\n' \
|
---|
463 | ' <ol class="tmgraph-buildcategories">\n';
|
---|
464 | for oBuildCat in self._oModel.getEligibleBuildCategories():
|
---|
465 | try: aidBuildCategories.remove(oBuildCat.idBuildCategory);
|
---|
466 | except: sChecked = '';
|
---|
467 | else: sChecked = ' checked';
|
---|
468 | sEnd += ' <li><input type="checkbox" name="%s" value="%s" id="gw-bc-%u" %s/>' \
|
---|
469 | '<label for="gw-bc-%u">%s / %s / %s / %s</label></li>\n' \
|
---|
470 | % ( WuiMain.ksParamGraphWizBuildCatIds, oBuildCat.idBuildCategory, oBuildCat.idBuildCategory, sChecked,
|
---|
471 | oBuildCat.idBuildCategory,
|
---|
472 | oBuildCat.sProduct, oBuildCat.sBranch, oBuildCat.sType, ' & '.join(oBuildCat.asOsArches) );
|
---|
473 | assert not aidBuildCategories; # SQL should return all currently selected.
|
---|
474 |
|
---|
475 | sEnd += ' </ol>\n' \
|
---|
476 | ' </div>\n';
|
---|
477 |
|
---|
478 | #
|
---|
479 | # Testcase variations.
|
---|
480 | #
|
---|
481 | sEnd += ' <div id="graphwiz-testcase-variations" class="graphwiz-end-selection-group">\n' \
|
---|
482 | ' <h3>Miscellaneous:</h3>\n' \
|
---|
483 | ' <ol>';
|
---|
484 |
|
---|
485 | sEnd += ' <li>\n' \
|
---|
486 | ' <input type="checkbox" id="%s" name="%s" value="1"%s/>\n' \
|
---|
487 | ' <label for="%s">Separate by testcase variation.</label>\n' \
|
---|
488 | ' </li>\n' \
|
---|
489 | % ( WuiMain.ksParamGraphWizSepTestVars, WuiMain.ksParamGraphWizSepTestVars,
|
---|
490 | ' checked' if self._dParams[WuiMain.ksParamGraphWizSepTestVars] else '',
|
---|
491 | WuiMain.ksParamGraphWizSepTestVars );
|
---|
492 |
|
---|
493 |
|
---|
494 | sEnd += ' <li>\n' \
|
---|
495 | ' <lable for="%s">Test case ID:</label>\n' \
|
---|
496 | ' <input type="text" id="%s" name="%s" value="%s" readonly/>\n' \
|
---|
497 | ' </li>\n' \
|
---|
498 | % ( WuiMain.ksParamGraphWizTestCaseIds,
|
---|
499 | WuiMain.ksParamGraphWizTestCaseIds, WuiMain.ksParamGraphWizTestCaseIds,
|
---|
500 | ','.join([str(i) for i in self._dParams[WuiMain.ksParamGraphWizTestCaseIds]]), );
|
---|
501 |
|
---|
502 | sEnd += ' </ol>\n' \
|
---|
503 | ' </div>\n';
|
---|
504 |
|
---|
505 | #sEnd += ' <h3> </h3>\n';
|
---|
506 |
|
---|
507 | #
|
---|
508 | # Finish up the form.
|
---|
509 | #
|
---|
510 | sEnd += ' <div id="graphwiz-end-submit"><p>' + sFormButton + '</p></div>\n';
|
---|
511 | sEnd += ' </div>\n' \
|
---|
512 | '</form>\n';
|
---|
513 |
|
---|
514 | return (sTop, sEnd);
|
---|
515 |
|
---|
516 | def generateReportBody(self):
|
---|
517 | fInteractive = not self._fSubReport;
|
---|
518 |
|
---|
519 | # Quick mockup.
|
---|
520 | self._sTitle = 'Graph Wizzard';
|
---|
521 |
|
---|
522 | sHtml = '';
|
---|
523 | sHtml += '<h2>Incomplete code - no complaints yet, thank you!!</h2>\n';
|
---|
524 |
|
---|
525 | #
|
---|
526 | # Create a form for altering the data we're working with.
|
---|
527 | #
|
---|
528 | if fInteractive:
|
---|
529 | (sTopOfForm, sEndOfForm) = self._generateInteractiveForm();
|
---|
530 | sHtml += sTopOfForm;
|
---|
531 | del sTopOfForm;
|
---|
532 |
|
---|
533 | #
|
---|
534 | # Emit the graphs. At least one per sample source.
|
---|
535 | #
|
---|
536 | sHtml += ' <div id="graphwiz-graphs">\n';
|
---|
537 | iGraph = 0;
|
---|
538 | aoCollections = self._oModel.fetchGraphData();
|
---|
539 | for iCollection, oCollection in enumerate(aoCollections):
|
---|
540 | # Name the graph and add a checkbox for removing it.
|
---|
541 | sSampleName = self._calcSampleName(oCollection);
|
---|
542 | sHtml += ' <div class="graphwiz-collection" id="graphwiz-source-%u">\n' % (iCollection,);
|
---|
543 | if fInteractive:
|
---|
544 | sHtml += ' <div class="graphwiz-src-select">\n' \
|
---|
545 | ' <input type="checkbox" name="%s" id="%s" value="%s:%s%s" checked class="graphwiz-src-input">\n' \
|
---|
546 | ' <label for="%s">%s</label>\n' \
|
---|
547 | ' </div>\n' \
|
---|
548 | % ( WuiMain.ksParamReportSubjectIds, WuiMain.ksParamReportSubjectIds, oCollection.sType,
|
---|
549 | ':'.join([str(idStr) for idStr in oCollection.aidStrTests]),
|
---|
550 | ':%u' % oCollection.idStrValue if oCollection.idStrValue else '',
|
---|
551 | WuiMain.ksParamReportSubjectIds, sSampleName );
|
---|
552 |
|
---|
553 | if oCollection.aoSeries:
|
---|
554 | #
|
---|
555 | # Split the series into sub-graphs as needed and produce SVGs.
|
---|
556 | #
|
---|
557 | aaoSeries = self._splitSeries(oCollection.aoSeries);
|
---|
558 | for aoSeries in aaoSeries:
|
---|
559 | # Gather the data for this graph. (Most big stuff is passed by
|
---|
560 | # reference, so there shouldn't be any large memory penalty for
|
---|
561 | # repacking the data here.)
|
---|
562 | sYUnit = None;
|
---|
563 | if aoSeries[0].iUnit < len(constants.valueunit.g_asNames) and aoSeries[0].iUnit > 0:
|
---|
564 | sYUnit = constants.valueunit.g_asNames[aoSeries[0].iUnit];
|
---|
565 | oData = WuiHlpGraphDataTableEx(sXUnit = 'Build revision', sYUnit = sYUnit);
|
---|
566 |
|
---|
567 | fSeriesName = self._figureSeriesNameBits(aoSeries);
|
---|
568 | for oSeries in aoSeries:
|
---|
569 | sSeriesName = self._getSeriesNameFromBits(oSeries, fSeriesName);
|
---|
570 | asHtmlTooltips = None;
|
---|
571 | if len(oSeries.aoRevInfo) == len(oSeries.aiRevisions):
|
---|
572 | asHtmlTooltips = [];
|
---|
573 | for i, oRevInfo in enumerate(oSeries.aoRevInfo):
|
---|
574 | sPlusMinus = '';
|
---|
575 | if oSeries.acSamples[i] > 1:
|
---|
576 | sPlusMinus = ' (+%s/-%s; %u samples)' \
|
---|
577 | % ( utils.formatNumber(oSeries.aiErrorBarAbove[i]),
|
---|
578 | utils.formatNumber(oSeries.aiErrorBarBelow[i]),
|
---|
579 | oSeries.acSamples[i])
|
---|
580 | sTooltip = '<table class=\'graphwiz-tt\'><tr><td>%s:</td><td>%s %s %s</td></tr>'\
|
---|
581 | '<tr><td>Rev:</td><td>r%s</td></tr>' \
|
---|
582 | % ( sSeriesName,
|
---|
583 | utils.formatNumber(oSeries.aiValues[i]),
|
---|
584 | sYUnit, sPlusMinus,
|
---|
585 | oSeries.aiRevisions[i],
|
---|
586 | );
|
---|
587 | if oRevInfo.sAuthor is not None:
|
---|
588 | sMsg = oRevInfo.sMessage[:80].strip();
|
---|
589 | #if sMsg.find('\n') >= 0:
|
---|
590 | # sMsg = sMsg[:sMsg.find('\n')].strip();
|
---|
591 | sTooltip += '<tr><td>Author:</td><td>%s</td></tr>' \
|
---|
592 | '<tr><td>Date:</td><td>%s</td><tr>' \
|
---|
593 | '<tr><td>Message:</td><td>%s%s</td></tr>' \
|
---|
594 | % ( oRevInfo.sAuthor,
|
---|
595 | self.formatTsShort(oRevInfo.tsCreated),
|
---|
596 | sMsg, '...' if len(oRevInfo.sMessage) > len(sMsg) else '');
|
---|
597 | sTooltip += '</table>';
|
---|
598 | asHtmlTooltips.append(sTooltip);
|
---|
599 | oData.addDataSeries(sSeriesName, oSeries.aiRevisions, oSeries.aiValues, asHtmlTooltips,
|
---|
600 | oSeries.aiErrorBarBelow, oSeries.aiErrorBarAbove);
|
---|
601 | # Render the data into a graph.
|
---|
602 | oGraph = self.oGraphClass('tmgraph-%u' % (iGraph,), oData, self._oDisp);
|
---|
603 | self._configureGraph(oGraph);
|
---|
604 |
|
---|
605 | oGraph.setTitle(self._calcGraphName(aoSeries[0], fSeriesName, sSampleName));
|
---|
606 | sHtml += ' <div class="graphwiz-graph" id="graphwiz-graph-%u">\n' % (iGraph,);
|
---|
607 | sHtml += oGraph.renderGraph();
|
---|
608 | sHtml += '\n </div>\n';
|
---|
609 | iGraph += 1;
|
---|
610 |
|
---|
611 | #
|
---|
612 | # Emit raw tabular data if requested.
|
---|
613 | #
|
---|
614 | if self._dParams[WuiMain.ksParamGraphWizTabular]:
|
---|
615 | sHtml += ' <div class="graphwiz-tab-div" id="graphwiz-tab-%u">\n' \
|
---|
616 | ' <table class="tmtable graphwiz-tab">\n' \
|
---|
617 | % (iCollection, );
|
---|
618 | for aoSeries in aaoSeries:
|
---|
619 | if aoSeries[0].iUnit < len(constants.valueunit.g_asNames) and aoSeries[0].iUnit > 0:
|
---|
620 | sUnit = constants.valueunit.g_asNames[aoSeries[0].iUnit];
|
---|
621 | else:
|
---|
622 | sUnit = str(aoSeries[0].iUnit);
|
---|
623 |
|
---|
624 | for iSeries, oSeries in enumerate(aoSeries):
|
---|
625 | sColor = self.oGraphClass.calcSeriesColor(iSeries);
|
---|
626 |
|
---|
627 | sHtml += '<thead class="tmheader">\n' \
|
---|
628 | ' <tr class="graphwiz-tab graphwiz-tab-new-series-row">\n' \
|
---|
629 | ' <th colspan="5"><span style="background-color:%s;"> </span> %s</th>\n' \
|
---|
630 | ' </tr>\n' \
|
---|
631 | ' <tr class="graphwiz-tab graphwiz-tab-col-hdr-row">\n' \
|
---|
632 | ' <th>Revision</th><th>Value (%s)</th><th>Δmax</th><th>Δmin</th>' \
|
---|
633 | '<th>Samples</th>\n' \
|
---|
634 | ' </tr>\n' \
|
---|
635 | '</thead>\n' \
|
---|
636 | % ( sColor,
|
---|
637 | self._getSeriesNameFromBits(oSeries, self.kfSeriesName_All & ~self.kfSeriesName_OsArchs),
|
---|
638 | sUnit );
|
---|
639 |
|
---|
640 | for i, iRevision in enumerate(oSeries.aiRevisions):
|
---|
641 | sHtml += ' <tr class="%s"><td>r%s</td><td>%s</td><td>+%s</td><td>-%s</td><td>%s</td></tr>\n' \
|
---|
642 | % ( 'tmodd' if i & 1 else 'tmeven',
|
---|
643 | iRevision, oSeries.aiValues[i],
|
---|
644 | oSeries.aiErrorBarAbove[i], oSeries.aiErrorBarBelow[i],
|
---|
645 | oSeries.acSamples[i]);
|
---|
646 | sHtml += ' </table>\n' \
|
---|
647 | ' </div>\n';
|
---|
648 | else:
|
---|
649 | sHtml += '<i>No results.</i>\n';
|
---|
650 | sHtml += ' </div>\n'
|
---|
651 | sHtml += ' </div>\n';
|
---|
652 |
|
---|
653 | #
|
---|
654 | # Finish the form.
|
---|
655 | #
|
---|
656 | if fInteractive:
|
---|
657 | sHtml += sEndOfForm;
|
---|
658 |
|
---|
659 | return sHtml;
|
---|
660 |
|
---|