1 | # -*- coding: utf-8 -*-
|
---|
2 | # $Id: testresults.py 53039 2014-10-13 10:49:38Z vboxsync $
|
---|
3 | # pylint: disable=C0302
|
---|
4 |
|
---|
5 | ## @todo Rename this file to testresult.py!
|
---|
6 |
|
---|
7 | """
|
---|
8 | Test Manager - Fetch test results.
|
---|
9 | """
|
---|
10 |
|
---|
11 | __copyright__ = \
|
---|
12 | """
|
---|
13 | Copyright (C) 2012-2014 Oracle Corporation
|
---|
14 |
|
---|
15 | This file is part of VirtualBox Open Source Edition (OSE), as
|
---|
16 | available from http://www.virtualbox.org. This file is free software;
|
---|
17 | you can redistribute it and/or modify it under the terms of the GNU
|
---|
18 | General Public License (GPL) as published by the Free Software
|
---|
19 | Foundation, in version 2 as it comes in the "COPYING" file of the
|
---|
20 | VirtualBox OSE distribution. VirtualBox OSE is distributed in the
|
---|
21 | hope that it will be useful, but WITHOUT ANY WARRANTY of any kind.
|
---|
22 |
|
---|
23 | The contents of this file may alternatively be used under the terms
|
---|
24 | of the Common Development and Distribution License Version 1.0
|
---|
25 | (CDDL) only, as it comes in the "COPYING.CDDL" file of the
|
---|
26 | VirtualBox OSE distribution, in which case the provisions of the
|
---|
27 | CDDL are applicable instead of those of the GPL.
|
---|
28 |
|
---|
29 | You may elect to license modified versions of this file under the
|
---|
30 | terms and conditions of either the GPL or the CDDL or both.
|
---|
31 | """
|
---|
32 | __version__ = "$Revision: 53039 $"
|
---|
33 | # Standard python imports.
|
---|
34 | import unittest;
|
---|
35 |
|
---|
36 | # Validation Kit imports.
|
---|
37 | from common import constants;
|
---|
38 | from testmanager import config;
|
---|
39 | from testmanager.core.base import ModelDataBase, ModelLogicBase, ModelDataBaseTestCase, TMExceptionBase, TMTooManyRows;
|
---|
40 | from testmanager.core.testgroup import TestGroupData
|
---|
41 | from testmanager.core.build import BuildDataEx
|
---|
42 | from testmanager.core.testbox import TestBoxData
|
---|
43 | from testmanager.core.testcase import TestCaseData
|
---|
44 | from testmanager.core.schedgroup import SchedGroupData
|
---|
45 | from testmanager.core.systemlog import SystemLogData, SystemLogLogic;
|
---|
46 |
|
---|
47 |
|
---|
48 | class TestResultData(ModelDataBase):
|
---|
49 | """
|
---|
50 | Test case execution result data
|
---|
51 | """
|
---|
52 |
|
---|
53 | ## @name TestStatus_T
|
---|
54 | # @{
|
---|
55 | ksTestStatus_Running = 'running';
|
---|
56 | ksTestStatus_Success = 'success';
|
---|
57 | ksTestStatus_Skipped = 'skipped';
|
---|
58 | ksTestStatus_BadTestBox = 'bad-testbox';
|
---|
59 | ksTestStatus_Aborted = 'aborted';
|
---|
60 | ksTestStatus_Failure = 'failure';
|
---|
61 | ksTestStatus_TimedOut = 'timed-out';
|
---|
62 | ksTestStatus_Rebooted = 'rebooted';
|
---|
63 | ## @}
|
---|
64 |
|
---|
65 | ## List of relatively harmless (to testgroup/case) statuses.
|
---|
66 | kasHarmlessTestStatuses = [ ksTestStatus_Skipped, ksTestStatus_BadTestBox, ksTestStatus_Aborted, ];
|
---|
67 | ## List of bad statuses.
|
---|
68 | kasBadTestStatuses = [ ksTestStatus_Failure, ksTestStatus_TimedOut, ksTestStatus_Rebooted, ];
|
---|
69 |
|
---|
70 |
|
---|
71 | ksIdAttr = 'idTestResult';
|
---|
72 |
|
---|
73 | ksParam_idTestResult = 'TestResultData_idTestResult';
|
---|
74 | ksParam_idTestResultParent = 'TestResultData_idTestResultParent';
|
---|
75 | ksParam_idTestSet = 'TestResultData_idTestSet';
|
---|
76 | ksParam_tsCreated = 'TestResultData_tsCreated';
|
---|
77 | ksParam_tsElapsed = 'TestResultData_tsElapsed';
|
---|
78 | ksParam_idStrName = 'TestResultData_idStrName';
|
---|
79 | ksParam_cErrors = 'TestResultData_cErrors';
|
---|
80 | ksParam_enmStatus = 'TestResultData_enmStatus';
|
---|
81 | ksParam_iNestingDepth = 'TestResultData_iNestingDepth';
|
---|
82 | kasValidValues_enmStatus = [
|
---|
83 | ksTestStatus_Running,
|
---|
84 | ksTestStatus_Success,
|
---|
85 | ksTestStatus_Skipped,
|
---|
86 | ksTestStatus_BadTestBox,
|
---|
87 | ksTestStatus_Aborted,
|
---|
88 | ksTestStatus_Failure,
|
---|
89 | ksTestStatus_TimedOut,
|
---|
90 | ksTestStatus_Rebooted
|
---|
91 | ];
|
---|
92 |
|
---|
93 |
|
---|
94 | def __init__(self):
|
---|
95 | ModelDataBase.__init__(self)
|
---|
96 | self.idTestResult = None
|
---|
97 | self.idTestResultParent = None
|
---|
98 | self.idTestSet = None
|
---|
99 | self.tsCreated = None
|
---|
100 | self.tsElapsed = None
|
---|
101 | self.idStrName = None
|
---|
102 | self.cErrors = 0;
|
---|
103 | self.enmStatus = None
|
---|
104 | self.iNestingDepth = None
|
---|
105 |
|
---|
106 | def initFromDbRow(self, aoRow):
|
---|
107 | """
|
---|
108 | Reinitialize from a SELECT * FROM TestResults.
|
---|
109 | Return self. Raises exception if no row.
|
---|
110 | """
|
---|
111 | if aoRow is None:
|
---|
112 | raise TMExceptionBase('Test result record not found.')
|
---|
113 |
|
---|
114 | self.idTestResult = aoRow[0]
|
---|
115 | self.idTestResultParent = aoRow[1]
|
---|
116 | self.idTestSet = aoRow[2]
|
---|
117 | self.tsCreated = aoRow[3]
|
---|
118 | self.tsElapsed = aoRow[4]
|
---|
119 | self.idStrName = aoRow[5]
|
---|
120 | self.cErrors = aoRow[6]
|
---|
121 | self.enmStatus = aoRow[7]
|
---|
122 | self.iNestingDepth = aoRow[8]
|
---|
123 | return self;
|
---|
124 |
|
---|
125 | def isFailure(self):
|
---|
126 | """ Check if it's a real failure. """
|
---|
127 | return self.enmStatus in self.kasBadTestStatuses;
|
---|
128 |
|
---|
129 |
|
---|
130 | class TestResultDataEx(TestResultData):
|
---|
131 | """
|
---|
132 | Extended test result data class.
|
---|
133 |
|
---|
134 | This is intended for use as a node in a result tree. This is not intended
|
---|
135 | for serialization to parameters or vice versa. Use TestResultLogic to
|
---|
136 | construct the tree.
|
---|
137 | """
|
---|
138 |
|
---|
139 | def __init__(self):
|
---|
140 | TestResultData.__init__(self)
|
---|
141 | self.sName = None; # idStrName resolved.
|
---|
142 | self.oParent = None; # idTestResultParent within the tree.
|
---|
143 |
|
---|
144 | self.aoChildren = []; # TestResultDataEx;
|
---|
145 | self.aoValues = []; # TestResultValue;
|
---|
146 | self.aoMsgs = []; # TestResultMsg;
|
---|
147 | self.aoFiles = []; # TestResultFile;
|
---|
148 |
|
---|
149 | def initFromDbRow(self, aoRow):
|
---|
150 | """
|
---|
151 | Initialize from a query like this:
|
---|
152 | SELECT TestResults.*, TestResultStrTab.sValue
|
---|
153 | FROM TestResults, TestResultStrTab
|
---|
154 | WHERE TestResultStrTab.idStr = TestResults.idStrName
|
---|
155 |
|
---|
156 | Note! The caller is expected to fetch children, values, failure
|
---|
157 | details, and files.
|
---|
158 | """
|
---|
159 | self.sName = None;
|
---|
160 | self.oParent = None;
|
---|
161 | self.aoChildren = [];
|
---|
162 | self.aoValues = [];
|
---|
163 | self.aoMsgs = [];
|
---|
164 | self.aoFiles = [];
|
---|
165 |
|
---|
166 | TestResultData.initFromDbRow(self, aoRow);
|
---|
167 |
|
---|
168 | self.sName = aoRow[9];
|
---|
169 | return self;
|
---|
170 |
|
---|
171 |
|
---|
172 | class TestResultValueData(ModelDataBase):
|
---|
173 | """
|
---|
174 | Test result value data.
|
---|
175 | """
|
---|
176 |
|
---|
177 | ksIdAttr = 'idTestResultValue';
|
---|
178 |
|
---|
179 | ksParam_idTestResultValue = 'TestResultValue_idTestResultValue';
|
---|
180 | ksParam_idTestResult = 'TestResultValue_idTestResult';
|
---|
181 | ksParam_idTestSet = 'TestResultValue_idTestSet';
|
---|
182 | ksParam_tsCreated = 'TestResultValue_tsCreated';
|
---|
183 | ksParam_idStrName = 'TestResultValue_idStrName';
|
---|
184 | ksParam_lValue = 'TestResultValue_lValue';
|
---|
185 | ksParam_iUnit = 'TestResultValue_iUnit';
|
---|
186 |
|
---|
187 | def __init__(self):
|
---|
188 | ModelDataBase.__init__(self)
|
---|
189 | self.idTestResultValue = None;
|
---|
190 | self.idTestResult = None;
|
---|
191 | self.idTestSet = None;
|
---|
192 | self.tsCreated = None;
|
---|
193 | self.idStrName = None;
|
---|
194 | self.lValue = None;
|
---|
195 | self.iUnit = 0;
|
---|
196 |
|
---|
197 | def initFromDbRow(self, aoRow):
|
---|
198 | """
|
---|
199 | Reinitialize from a SELECT * FROM TestResultValues.
|
---|
200 | Return self. Raises exception if no row.
|
---|
201 | """
|
---|
202 | if aoRow is None:
|
---|
203 | raise TMExceptionBase('Test result value record not found.')
|
---|
204 |
|
---|
205 | self.idTestResultValue = aoRow[0];
|
---|
206 | self.idTestResult = aoRow[1];
|
---|
207 | self.idTestSet = aoRow[2];
|
---|
208 | self.tsCreated = aoRow[3];
|
---|
209 | self.idStrName = aoRow[4];
|
---|
210 | self.lValue = aoRow[5];
|
---|
211 | self.iUnit = aoRow[6];
|
---|
212 | return self;
|
---|
213 |
|
---|
214 |
|
---|
215 | class TestResultValueDataEx(TestResultValueData):
|
---|
216 | """
|
---|
217 | Extends TestResultValue by resolving the value name and unit string.
|
---|
218 | """
|
---|
219 |
|
---|
220 | def __init__(self):
|
---|
221 | TestResultValueData.__init__(self)
|
---|
222 | self.sName = None;
|
---|
223 | self.sUnit = '';
|
---|
224 |
|
---|
225 | def initFromDbRow(self, aoRow):
|
---|
226 | """
|
---|
227 | Reinitialize from a query like this:
|
---|
228 | SELECT TestResultValues.*, TestResultStrTab.sValue
|
---|
229 | FROM TestResultValues, TestResultStrTab
|
---|
230 | WHERE TestResultStrTab.idStr = TestResultValues.idStrName
|
---|
231 |
|
---|
232 | Return self. Raises exception if no row.
|
---|
233 | """
|
---|
234 | TestResultValueData.initFromDbRow(self, aoRow);
|
---|
235 | self.sName = aoRow[7];
|
---|
236 | if self.iUnit < len(constants.valueunit.g_asNames):
|
---|
237 | self.sUnit = constants.valueunit.g_asNames[self.iUnit];
|
---|
238 | else:
|
---|
239 | self.sUnit = '<%d>' % (self.iUnit,);
|
---|
240 | return self;
|
---|
241 |
|
---|
242 | class TestResultMsgData(ModelDataBase):
|
---|
243 | """
|
---|
244 | Test result message data.
|
---|
245 | """
|
---|
246 |
|
---|
247 | ksIdAttr = 'idTestResultMsg';
|
---|
248 |
|
---|
249 | ksParam_idTestResultMsg = 'TestResultValue_idTestResultMsg';
|
---|
250 | ksParam_idTestResult = 'TestResultValue_idTestResult';
|
---|
251 | ksParam_tsCreated = 'TestResultValue_tsCreated';
|
---|
252 | ksParam_idStrMsg = 'TestResultValue_idStrMsg';
|
---|
253 | ksParam_enmLevel = 'TestResultValue_enmLevel';
|
---|
254 |
|
---|
255 | def __init__(self):
|
---|
256 | ModelDataBase.__init__(self)
|
---|
257 | self.idTestResultMsg = None;
|
---|
258 | self.idTestResult = None;
|
---|
259 | self.tsCreated = None;
|
---|
260 | self.idStrMsg = None;
|
---|
261 | self.enmLevel = None;
|
---|
262 |
|
---|
263 | def initFromDbRow(self, aoRow):
|
---|
264 | """
|
---|
265 | Reinitialize from a SELECT * FROM TestResultMsgs.
|
---|
266 | Return self. Raises exception if no row.
|
---|
267 | """
|
---|
268 | if aoRow is None:
|
---|
269 | raise TMExceptionBase('Test result value record not found.')
|
---|
270 |
|
---|
271 | self.idTestResultMsg = aoRow[0];
|
---|
272 | self.idTestResult = aoRow[1];
|
---|
273 | self.tsCreated = aoRow[2];
|
---|
274 | self.idStrMsg = aoRow[3];
|
---|
275 | self.enmLevel = aoRow[4];
|
---|
276 | return self;
|
---|
277 |
|
---|
278 | class TestResultMsgDataEx(TestResultMsgData):
|
---|
279 | """
|
---|
280 | Extends TestResultMsg by resolving the message string.
|
---|
281 | """
|
---|
282 |
|
---|
283 | def __init__(self):
|
---|
284 | TestResultMsgData.__init__(self)
|
---|
285 | self.sMsg = None;
|
---|
286 |
|
---|
287 | def initFromDbRow(self, aoRow):
|
---|
288 | """
|
---|
289 | Reinitialize from a query like this:
|
---|
290 | SELECT TestResultMsg.*, TestResultStrTab.sValue
|
---|
291 | FROM TestResultMsg, TestResultStrTab
|
---|
292 | WHERE TestResultStrTab.idStr = TestResultMsgs.idStrName
|
---|
293 |
|
---|
294 | Return self. Raises exception if no row.
|
---|
295 | """
|
---|
296 | TestResultMsgData.initFromDbRow(self, aoRow);
|
---|
297 | self.sMsg = aoRow[5];
|
---|
298 | return self;
|
---|
299 |
|
---|
300 | class TestResultFileData(ModelDataBase):
|
---|
301 | """
|
---|
302 | Test result message data.
|
---|
303 | """
|
---|
304 |
|
---|
305 | ksIdAttr = 'idTestResultFile';
|
---|
306 |
|
---|
307 | ksParam_idTestResultFile = 'TestResultFile_idTestResultFile';
|
---|
308 | ksParam_idTestResult = 'TestResultFile_idTestResult';
|
---|
309 | ksParam_tsCreated = 'TestResultFile_tsCreated';
|
---|
310 | ksParam_idStrFile = 'TestResultFile_idStrFile';
|
---|
311 | ksParam_idStrDescription = 'TestResultFile_idStrDescription';
|
---|
312 | ksParam_idStrKind = 'TestResultFile_idStrKind';
|
---|
313 | ksParam_idStrMime = 'TestResultFile_idStrMime';
|
---|
314 |
|
---|
315 | def __init__(self):
|
---|
316 | ModelDataBase.__init__(self)
|
---|
317 | self.idTestResultFile = None;
|
---|
318 | self.idTestResult = None;
|
---|
319 | self.tsCreated = None;
|
---|
320 | self.idStrFile = None;
|
---|
321 | self.idStrDescription = None;
|
---|
322 | self.idStrKind = None;
|
---|
323 | self.idStrMime = None;
|
---|
324 |
|
---|
325 | def initFromDbRow(self, aoRow):
|
---|
326 | """
|
---|
327 | Reinitialize from a SELECT * FROM TestResultFiles.
|
---|
328 | Return self. Raises exception if no row.
|
---|
329 | """
|
---|
330 | if aoRow is None:
|
---|
331 | raise TMExceptionBase('Test result file record not found.')
|
---|
332 |
|
---|
333 | self.idTestResultFile = aoRow[0];
|
---|
334 | self.idTestResult = aoRow[1];
|
---|
335 | self.tsCreated = aoRow[2];
|
---|
336 | self.idStrFile = aoRow[3];
|
---|
337 | self.idStrDescription = aoRow[4];
|
---|
338 | self.idStrKind = aoRow[5];
|
---|
339 | self.idStrMime = aoRow[6];
|
---|
340 | return self;
|
---|
341 |
|
---|
342 | class TestResultFileDataEx(TestResultFileData):
|
---|
343 | """
|
---|
344 | Extends TestResultFile by resolving the strings.
|
---|
345 | """
|
---|
346 |
|
---|
347 | def __init__(self):
|
---|
348 | TestResultFileData.__init__(self)
|
---|
349 | self.sFile = None;
|
---|
350 | self.sDescription = None;
|
---|
351 | self.sKind = None;
|
---|
352 | self.sMime = None;
|
---|
353 |
|
---|
354 | def initFromDbRow(self, aoRow):
|
---|
355 | """
|
---|
356 | Reinitialize from a query like this:
|
---|
357 | SELECT TestResultFiles.*,
|
---|
358 | StrTabFile.sValue AS sFile,
|
---|
359 | StrTabDesc.sValue AS sDescription
|
---|
360 | StrTabKind.sValue AS sKind,
|
---|
361 | StrTabMime.sValue AS sMime,
|
---|
362 | FROM ...
|
---|
363 |
|
---|
364 | Return self. Raises exception if no row.
|
---|
365 | """
|
---|
366 | TestResultFileData.initFromDbRow(self, aoRow);
|
---|
367 | self.sFile = aoRow[7];
|
---|
368 | self.sDescription = aoRow[8];
|
---|
369 | self.sKind = aoRow[9];
|
---|
370 | self.sMime = aoRow[10];
|
---|
371 | return self;
|
---|
372 |
|
---|
373 | def initFakeMainLog(self, oTestSet):
|
---|
374 | """
|
---|
375 | Reinitializes to represent the main.log object (not in DB).
|
---|
376 |
|
---|
377 | Returns self.
|
---|
378 | """
|
---|
379 | self.idTestResultFile = 0;
|
---|
380 | self.idTestResult = oTestSet.idTestResult;
|
---|
381 | self.tsCreated = oTestSet.tsCreated;
|
---|
382 | self.idStrFile = None;
|
---|
383 | self.idStrDescription = None;
|
---|
384 | self.idStrKind = None;
|
---|
385 | self.idStrMime = None;
|
---|
386 |
|
---|
387 | self.sFile = 'main.log';
|
---|
388 | self.sDescription = '';
|
---|
389 | self.sKind = 'log/main';
|
---|
390 | self.sMime = 'text/plain';
|
---|
391 | return self;
|
---|
392 |
|
---|
393 | def isProbablyUtf8Encoded(self):
|
---|
394 | """
|
---|
395 | Checks if the file is likely to be UTF-8 encoded.
|
---|
396 | """
|
---|
397 | if self.sMime in [ 'text/plain', 'text/html' ]:
|
---|
398 | return True;
|
---|
399 | return False;
|
---|
400 |
|
---|
401 | def getMimeWithEncoding(self):
|
---|
402 | """
|
---|
403 | Gets the MIME type with encoding if likely to be UTF-8.
|
---|
404 | """
|
---|
405 | if self.isProbablyUtf8Encoded():
|
---|
406 | return '%s; charset=utf-8' % (self.sMime,);
|
---|
407 | return self.sMime;
|
---|
408 |
|
---|
409 |
|
---|
410 | class TestResultListingData(ModelDataBase): # pylint: disable=R0902
|
---|
411 | """
|
---|
412 | Test case result data representation for table listing
|
---|
413 | """
|
---|
414 |
|
---|
415 | def __init__(self):
|
---|
416 | """Initialize"""
|
---|
417 | ModelDataBase.__init__(self)
|
---|
418 |
|
---|
419 | self.idTestSet = None
|
---|
420 |
|
---|
421 | self.idBuildCategory = None;
|
---|
422 | self.sProduct = None
|
---|
423 | self.sRepository = None;
|
---|
424 | self.sBranch = None
|
---|
425 | self.sType = None
|
---|
426 | self.idBuild = None;
|
---|
427 | self.sVersion = None;
|
---|
428 | self.iRevision = None
|
---|
429 |
|
---|
430 | self.sOs = None;
|
---|
431 | self.sOsVersion = None;
|
---|
432 | self.sArch = None;
|
---|
433 | self.sCpuVendor = None;
|
---|
434 | self.sCpuName = None;
|
---|
435 | self.cCpus = None;
|
---|
436 | self.fCpuHwVirt = None;
|
---|
437 | self.fCpuNestedPaging = None;
|
---|
438 | self.fCpu64BitGuest = None;
|
---|
439 | self.idTestBox = None
|
---|
440 | self.sTestBoxName = None
|
---|
441 |
|
---|
442 | self.tsCreated = None
|
---|
443 | self.tsElapsed = None
|
---|
444 | self.enmStatus = None
|
---|
445 | self.cErrors = None;
|
---|
446 |
|
---|
447 | self.idTestCase = None
|
---|
448 | self.sTestCaseName = None
|
---|
449 | self.sBaseCmd = None
|
---|
450 | self.sArgs = None
|
---|
451 |
|
---|
452 | self.idBuildTestSuite = None;
|
---|
453 | self.iRevisionTestSuite = None;
|
---|
454 |
|
---|
455 | def initFromDbRow(self, aoRow):
|
---|
456 | """
|
---|
457 | Reinitialize from a database query.
|
---|
458 | Return self. Raises exception if no row.
|
---|
459 | """
|
---|
460 | if aoRow is None:
|
---|
461 | raise TMExceptionBase('Test result record not found.')
|
---|
462 |
|
---|
463 | self.idTestSet = aoRow[0];
|
---|
464 |
|
---|
465 | self.idBuildCategory = aoRow[1];
|
---|
466 | self.sProduct = aoRow[2];
|
---|
467 | self.sRepository = aoRow[3];
|
---|
468 | self.sBranch = aoRow[4];
|
---|
469 | self.sType = aoRow[5];
|
---|
470 | self.idBuild = aoRow[6];
|
---|
471 | self.sVersion = aoRow[7];
|
---|
472 | self.iRevision = aoRow[8];
|
---|
473 |
|
---|
474 | self.sOs = aoRow[9];
|
---|
475 | self.sOsVersion = aoRow[10];
|
---|
476 | self.sArch = aoRow[11];
|
---|
477 | self.sCpuVendor = aoRow[12];
|
---|
478 | self.sCpuName = aoRow[13];
|
---|
479 | self.cCpus = aoRow[14];
|
---|
480 | self.fCpuHwVirt = aoRow[15];
|
---|
481 | self.fCpuNestedPaging = aoRow[16];
|
---|
482 | self.fCpu64BitGuest = aoRow[17];
|
---|
483 | self.idTestBox = aoRow[18];
|
---|
484 | self.sTestBoxName = aoRow[19];
|
---|
485 |
|
---|
486 | self.tsCreated = aoRow[20];
|
---|
487 | self.tsElapsed = aoRow[21];
|
---|
488 | self.enmStatus = aoRow[22];
|
---|
489 | self.cErrors = aoRow[23];
|
---|
490 |
|
---|
491 | self.idTestCase = aoRow[24];
|
---|
492 | self.sTestCaseName = aoRow[25];
|
---|
493 | self.sBaseCmd = aoRow[26];
|
---|
494 | self.sArgs = aoRow[27];
|
---|
495 |
|
---|
496 | self.idBuildTestSuite = aoRow[28];
|
---|
497 | self.iRevisionTestSuite = aoRow[29];
|
---|
498 |
|
---|
499 | return self
|
---|
500 |
|
---|
501 |
|
---|
502 | class TestResultHangingOffence(TMExceptionBase):
|
---|
503 | """Hanging offence committed by test case."""
|
---|
504 | pass;
|
---|
505 |
|
---|
506 | class TestResultLogic(ModelLogicBase): # pylint: disable=R0903
|
---|
507 | """
|
---|
508 | Results grouped by scheduling group.
|
---|
509 | """
|
---|
510 |
|
---|
511 | #
|
---|
512 | # Result grinding for displaying in the WUI.
|
---|
513 | #
|
---|
514 |
|
---|
515 | ksResultsGroupingTypeNone = 'ResultsGroupingTypeNone'
|
---|
516 | ksResultsGroupingTypeTestGroup = 'ResultsGroupingTypeTestGroup'
|
---|
517 | ksResultsGroupingTypeBuildRev = 'ResultsGroupingTypeBuild'
|
---|
518 | ksResultsGroupingTypeTestBox = 'ResultsGroupingTypeTestBox'
|
---|
519 | ksResultsGroupingTypeTestCase = 'ResultsGroupingTypeTestCase'
|
---|
520 | ksResultsGroupingTypeSchedGroup = 'ResultsGroupingTypeSchedGroup'
|
---|
521 |
|
---|
522 | ksBaseTables = 'BuildCategories, Builds, TestBoxes, TestResults, TestCases, TestCaseArgs,\n' \
|
---|
523 | + ' TestSets LEFT OUTER JOIN Builds AS TestSuiteBits\n' \
|
---|
524 | ' ON TestSets.idBuildTestSuite = TestSuiteBits.idBuild\n';
|
---|
525 |
|
---|
526 | ksBasePreCondition = 'TestSets.idTestSet = TestResults.idTestSet\n' \
|
---|
527 | + ' AND TestResults.idTestResultParent is NULL\n' \
|
---|
528 | + ' AND TestSets.idBuild = Builds.idBuild\n' \
|
---|
529 | + ' AND Builds.tsExpire > TestSets.tsCreated\n' \
|
---|
530 | + ' AND Builds.tsEffective <= TestSets.tsCreated\n' \
|
---|
531 | + ' AND Builds.idBuildCategory = BuildCategories.idBuildCategory\n' \
|
---|
532 | + ' AND TestSets.idGenTestBox = TestBoxes.idGenTestBox\n' \
|
---|
533 | + ' AND TestSets.idGenTestCase = TestCases.idGenTestCase\n' \
|
---|
534 | + ' AND TestSets.idGenTestCaseArgs = TestCaseArgs.idGenTestCaseArgs\n'
|
---|
535 | kdResultGroupingMap = {
|
---|
536 | ksResultsGroupingTypeNone: (ksBaseTables,
|
---|
537 | ksBasePreCondition,),
|
---|
538 |
|
---|
539 | ksResultsGroupingTypeTestGroup: (ksBaseTables,
|
---|
540 | ksBasePreCondition + ' AND TestSets.idTestGroup',),
|
---|
541 |
|
---|
542 | ksResultsGroupingTypeBuildRev: (ksBaseTables,
|
---|
543 | ksBasePreCondition + ' AND Builds.iRevision',),
|
---|
544 |
|
---|
545 | ksResultsGroupingTypeTestBox: (ksBaseTables,
|
---|
546 | ksBasePreCondition + ' AND TestSets.idTestBox',),
|
---|
547 |
|
---|
548 | ksResultsGroupingTypeTestCase: (ksBaseTables,
|
---|
549 | ksBasePreCondition + ' AND TestSets.idTestCase',),
|
---|
550 |
|
---|
551 | ksResultsGroupingTypeSchedGroup: (ksBaseTables,
|
---|
552 | ksBasePreCondition + ' AND TestBoxes.idSchedGroup',),
|
---|
553 | }
|
---|
554 |
|
---|
555 | def _getTimePeriodQueryPart(self, tsNow, sInterval):
|
---|
556 | """
|
---|
557 | Get part of SQL query responsible for SELECT data within
|
---|
558 | specified period of time.
|
---|
559 | """
|
---|
560 | assert sInterval is not None; # too many rows.
|
---|
561 |
|
---|
562 | cMonthsMourningPeriod = 2; # Stop reminding everyone about testboxes after 2 months. (May also speed up the query.)
|
---|
563 | if tsNow is None:
|
---|
564 | sRet = '(TestSets.tsDone IS NULL OR TestSets.tsDone >= (CURRENT_TIMESTAMP - \'%s\'::interval))\n' \
|
---|
565 | ' AND TestSets.tsCreated >= (CURRENT_TIMESTAMP - \'%s\'::interval - \'%u months\'::interval)\n' \
|
---|
566 | % (sInterval, sInterval, cMonthsMourningPeriod);
|
---|
567 | else:
|
---|
568 | sTsNow = '\'%s\'::TIMESTAMP' % (tsNow,); # It's actually a string already. duh.
|
---|
569 | sRet = 'TestSets.tsCreated <= %s\n' \
|
---|
570 | ' AND TestSets.tsCreated >= (%s - \'%s\'::interval - \'%u months\'::interval)\n' \
|
---|
571 | ' AND (TestSets.tsDone IS NULL OR TestSets.tsDone >= (%s - \'%s\'::interval))\n' \
|
---|
572 | % ( sTsNow,
|
---|
573 | sTsNow, sInterval, cMonthsMourningPeriod,
|
---|
574 | sTsNow, sInterval );
|
---|
575 | return sRet
|
---|
576 |
|
---|
577 | def _getSqlQueryForGroupSearch(self, sWhat, tsNow, sInterval, enmResultsGroupingType, iResultsGroupingValue, fOnlyFailures):
|
---|
578 | """
|
---|
579 | Returns an SQL query that limits SELECT result
|
---|
580 | in order to satisfy @param enmResultsGroupingType.
|
---|
581 | """
|
---|
582 |
|
---|
583 | if enmResultsGroupingType is None:
|
---|
584 | raise TMExceptionBase('Unknown grouping type')
|
---|
585 |
|
---|
586 | if enmResultsGroupingType not in self.kdResultGroupingMap:
|
---|
587 | raise TMExceptionBase('Unknown grouping type')
|
---|
588 |
|
---|
589 | # Get SQL query parameters
|
---|
590 | sTables, sCondition = self.kdResultGroupingMap[enmResultsGroupingType]
|
---|
591 |
|
---|
592 | # Extend SQL query with time period limitation
|
---|
593 | sTimePeriodQuery = self._getTimePeriodQueryPart(tsNow, sInterval)
|
---|
594 |
|
---|
595 | if iResultsGroupingValue is not None:
|
---|
596 | sCondition += ' = %d' % iResultsGroupingValue + '\n';
|
---|
597 | sCondition += ' AND ' + sTimePeriodQuery
|
---|
598 |
|
---|
599 | # Extend the condition with test status limitations if requested.
|
---|
600 | if fOnlyFailures:
|
---|
601 | sCondition += '\n AND TestSets.enmStatus != \'success\'::TestStatus_T' \
|
---|
602 | '\n AND TestSets.enmStatus != \'running\'::TestStatus_T';
|
---|
603 |
|
---|
604 | # Assemble the query.
|
---|
605 | sQuery = 'SELECT DISTINCT %s\n' % sWhat
|
---|
606 | sQuery += 'FROM %s\n' % sTables
|
---|
607 | sQuery += 'WHERE %s\n' % sCondition
|
---|
608 |
|
---|
609 | return sQuery
|
---|
610 |
|
---|
611 | def fetchResultsForListing(self, iStart, cMaxRows, tsNow, sInterval, enmResultsGroupingType, iResultsGroupingValue,
|
---|
612 | fOnlyFailures):
|
---|
613 | """
|
---|
614 | Fetches TestResults table content.
|
---|
615 |
|
---|
616 | If @param enmResultsGroupingType and @param iResultsGroupingValue
|
---|
617 | are not None, then resulting (returned) list contains only records
|
---|
618 | that match specified @param enmResultsGroupingType.
|
---|
619 |
|
---|
620 | If @param enmResultsGroupingType is None, then
|
---|
621 | @param iResultsGroupingValue is ignored.
|
---|
622 |
|
---|
623 | Returns an array (list) of TestResultData items, empty list if none.
|
---|
624 | Raises exception on error.
|
---|
625 | """
|
---|
626 |
|
---|
627 | sWhat = 'TestSets.idTestSet,\n' \
|
---|
628 | ' BuildCategories.idBuildCategory,\n' \
|
---|
629 | ' BuildCategories.sProduct,\n' \
|
---|
630 | ' BuildCategories.sRepository,\n' \
|
---|
631 | ' BuildCategories.sBranch,\n' \
|
---|
632 | ' BuildCategories.sType,\n' \
|
---|
633 | ' Builds.idBuild,\n' \
|
---|
634 | ' Builds.sVersion,\n' \
|
---|
635 | ' Builds.iRevision,\n' \
|
---|
636 | ' TestBoxes.sOs,\n' \
|
---|
637 | ' TestBoxes.sOsVersion,\n' \
|
---|
638 | ' TestBoxes.sCpuArch,\n' \
|
---|
639 | ' TestBoxes.sCpuVendor,\n' \
|
---|
640 | ' TestBoxes.sCpuName,\n' \
|
---|
641 | ' TestBoxes.cCpus,\n' \
|
---|
642 | ' TestBoxes.fCpuHwVirt,\n' \
|
---|
643 | ' TestBoxes.fCpuNestedPaging,\n' \
|
---|
644 | ' TestBoxes.fCpu64BitGuest,\n' \
|
---|
645 | ' TestBoxes.idTestBox,\n' \
|
---|
646 | ' TestBoxes.sName,\n' \
|
---|
647 | ' TestResults.tsCreated,\n' \
|
---|
648 | ' COALESCE(TestResults.tsElapsed, CURRENT_TIMESTAMP - TestResults.tsCreated),\n' \
|
---|
649 | ' TestSets.enmStatus,\n' \
|
---|
650 | ' TestResults.cErrors,\n' \
|
---|
651 | ' TestCases.idTestCase,\n' \
|
---|
652 | ' TestCases.sName,\n' \
|
---|
653 | ' TestCases.sBaseCmd,\n' \
|
---|
654 | ' TestCaseArgs.sArgs,\n' \
|
---|
655 | ' TestSuiteBits.idBuild AS idBuildTestSuite,\n' \
|
---|
656 | ' TestSuiteBits.iRevision AS iRevisionTestSuite,\n' \
|
---|
657 | ' (TestSets.tsDone IS NULL) SortRunningFirst' \
|
---|
658 | ;
|
---|
659 |
|
---|
660 | sSqlQuery = self._getSqlQueryForGroupSearch(sWhat, tsNow, sInterval, enmResultsGroupingType, iResultsGroupingValue,
|
---|
661 | fOnlyFailures);
|
---|
662 |
|
---|
663 | sSqlQuery += 'ORDER BY SortRunningFirst DESC, TestSets.idTestSet DESC\n';
|
---|
664 | sSqlQuery += 'LIMIT %s OFFSET %s\n' % (cMaxRows, iStart,);
|
---|
665 |
|
---|
666 | self._oDb.execute(sSqlQuery);
|
---|
667 |
|
---|
668 | aoRows = [];
|
---|
669 | for aoRow in self._oDb.fetchAll():
|
---|
670 | aoRows.append(TestResultListingData().initFromDbRow(aoRow))
|
---|
671 |
|
---|
672 | return aoRows
|
---|
673 |
|
---|
674 | def getEntriesCount(self, tsNow, sInterval, enmResultsGroupingType, iResultsGroupingValue, fOnlyFailures):
|
---|
675 | """
|
---|
676 | Get number of table records.
|
---|
677 |
|
---|
678 | If @param enmResultsGroupingType and @param iResultsGroupingValue
|
---|
679 | are not None, then we count only only those records
|
---|
680 | that match specified @param enmResultsGroupingType.
|
---|
681 |
|
---|
682 | If @param enmResultsGroupingType is None, then
|
---|
683 | @param iResultsGroupingValue is ignored.
|
---|
684 | """
|
---|
685 |
|
---|
686 | sSqlQuery = self._getSqlQueryForGroupSearch('COUNT(TestSets.idTestSet)', tsNow, sInterval,
|
---|
687 | enmResultsGroupingType, iResultsGroupingValue, fOnlyFailures)
|
---|
688 | self._oDb.execute(sSqlQuery)
|
---|
689 | return self._oDb.fetchOne()[0]
|
---|
690 |
|
---|
691 | def getTestGroups(self, tsNow, sPeriod):
|
---|
692 | """
|
---|
693 | Get list of uniq TestGroupData objects which
|
---|
694 | found in all test results.
|
---|
695 | """
|
---|
696 |
|
---|
697 | self._oDb.execute('SELECT DISTINCT TestGroups.*\n'
|
---|
698 | 'FROM TestGroups, TestSets\n'
|
---|
699 | 'WHERE TestSets.idTestGroup = TestGroups.idTestGroup\n'
|
---|
700 | ' AND TestGroups.tsExpire > TestSets.tsCreated\n'
|
---|
701 | ' AND TestGroups.tsEffective <= TestSets.tsCreated'
|
---|
702 | ' AND ' + self._getTimePeriodQueryPart(tsNow, sPeriod))
|
---|
703 |
|
---|
704 | aaoRows = self._oDb.fetchAll()
|
---|
705 | aoRet = []
|
---|
706 | for aoRow in aaoRows:
|
---|
707 | ## @todo Need to take time into consideration. Will go belly up if we delete a testgroup.
|
---|
708 | aoRet.append(TestGroupData().initFromDbRow(aoRow))
|
---|
709 |
|
---|
710 | return aoRet
|
---|
711 |
|
---|
712 | def getBuilds(self, tsNow, sPeriod):
|
---|
713 | """
|
---|
714 | Get list of uniq BuildDataEx objects which
|
---|
715 | found in all test results.
|
---|
716 | """
|
---|
717 |
|
---|
718 | self._oDb.execute('SELECT DISTINCT Builds.*, BuildCategories.*\n'
|
---|
719 | 'FROM Builds, BuildCategories, TestSets\n'
|
---|
720 | 'WHERE TestSets.idBuild = Builds.idBuild\n'
|
---|
721 | ' AND Builds.idBuildCategory = BuildCategories.idBuildCategory\n'
|
---|
722 | ' AND Builds.tsExpire > TestSets.tsCreated\n'
|
---|
723 | ' AND Builds.tsEffective <= TestSets.tsCreated'
|
---|
724 | ' AND ' + self._getTimePeriodQueryPart(tsNow, sPeriod))
|
---|
725 |
|
---|
726 | aaoRows = self._oDb.fetchAll()
|
---|
727 | aoRet = []
|
---|
728 | for aoRow in aaoRows:
|
---|
729 | aoRet.append(BuildDataEx().initFromDbRow(aoRow))
|
---|
730 |
|
---|
731 | return aoRet
|
---|
732 |
|
---|
733 | def getTestBoxes(self, tsNow, sPeriod):
|
---|
734 | """
|
---|
735 | Get list of uniq TestBoxData objects which
|
---|
736 | found in all test results.
|
---|
737 | """
|
---|
738 |
|
---|
739 | ## @todo do all in one query.
|
---|
740 | self._oDb.execute('SELECT DISTINCT TestBoxes.idTestBox, TestBoxes.idGenTestBox\n'
|
---|
741 | 'FROM TestBoxes, TestSets\n'
|
---|
742 | 'WHERE TestSets.idGenTestBox = TestBoxes.idGenTestBox\n'
|
---|
743 | ' AND ' + self._getTimePeriodQueryPart(tsNow, sPeriod) +
|
---|
744 | 'ORDER BY TestBoxes.idTestBox, TestBoxes.idGenTestBox DESC' );
|
---|
745 | idPrevTestBox = -1;
|
---|
746 | asIdGenTestBoxes = [];
|
---|
747 | for aoRow in self._oDb.fetchAll():
|
---|
748 | if aoRow[0] != idPrevTestBox:
|
---|
749 | idPrevTestBox = aoRow[0];
|
---|
750 | asIdGenTestBoxes.append(str(aoRow[1]));
|
---|
751 |
|
---|
752 | aoRet = []
|
---|
753 | if len(asIdGenTestBoxes) > 0:
|
---|
754 | self._oDb.execute('SELECT *\n'
|
---|
755 | 'FROM TestBoxes\n'
|
---|
756 | 'WHERE idGenTestBox IN (' + ','.join(asIdGenTestBoxes) + ')\n'
|
---|
757 | 'ORDER BY sName');
|
---|
758 | for aoRow in self._oDb.fetchAll():
|
---|
759 | aoRet.append(TestBoxData().initFromDbRow(aoRow));
|
---|
760 | return aoRet
|
---|
761 |
|
---|
762 | def getTestCases(self, tsNow, sPeriod):
|
---|
763 | """
|
---|
764 | Get a list of unique TestCaseData objects which is appears in the test
|
---|
765 | specified result period.
|
---|
766 | """
|
---|
767 |
|
---|
768 | self._oDb.execute('SELECT DISTINCT TestCases.idTestCase, TestCases.idGenTestCase, TestSets.tsConfig\n'
|
---|
769 | 'FROM TestCases, TestSets\n'
|
---|
770 | 'WHERE TestSets.idTestCase = TestCases.idTestCase\n'
|
---|
771 | ' AND TestCases.tsExpire > TestSets.tsCreated\n'
|
---|
772 | ' AND TestCases.tsEffective <= TestSets.tsCreated\n'
|
---|
773 | ' AND ' + self._getTimePeriodQueryPart(tsNow, sPeriod) +
|
---|
774 | 'ORDER BY TestCases.idTestCase, TestCases.idGenTestCase DESC\n');
|
---|
775 |
|
---|
776 | aaoRows = self._oDb.fetchAll()
|
---|
777 | aoRet = []
|
---|
778 | idPrevTestCase = -1;
|
---|
779 | for aoRow in aaoRows:
|
---|
780 | ## @todo reduce subqueries
|
---|
781 | if aoRow[0] != idPrevTestCase:
|
---|
782 | idPrevTestCase = aoRow[0];
|
---|
783 | aoRet.append(TestCaseData().initFromDbWithGenId(self._oDb, aoRow[1], aoRow[2]))
|
---|
784 |
|
---|
785 | return aoRet
|
---|
786 |
|
---|
787 | def getSchedGroups(self, tsNow, sPeriod):
|
---|
788 | """
|
---|
789 | Get list of uniq SchedGroupData objects which
|
---|
790 | found in all test results.
|
---|
791 | """
|
---|
792 |
|
---|
793 | self._oDb.execute('SELECT DISTINCT TestBoxes.idSchedGroup\n'
|
---|
794 | 'FROM TestBoxes, TestSets\n'
|
---|
795 | 'WHERE TestSets.idGenTestBox = TestBoxes.idGenTestBox\n'
|
---|
796 | ' AND TestBoxes.tsExpire > TestSets.tsCreated\n'
|
---|
797 | ' AND TestBoxes.tsEffective <= TestSets.tsCreated'
|
---|
798 | ' AND ' + self._getTimePeriodQueryPart(tsNow, sPeriod))
|
---|
799 |
|
---|
800 | aiRows = self._oDb.fetchAll()
|
---|
801 | aoRet = []
|
---|
802 | for iRow in aiRows:
|
---|
803 | ## @todo reduce subqueries
|
---|
804 | aoRet.append(SchedGroupData().initFromDbWithId(self._oDb, iRow))
|
---|
805 |
|
---|
806 | return aoRet
|
---|
807 |
|
---|
808 | def getById(self, idTestResult):
|
---|
809 | """
|
---|
810 | Get build record by its id
|
---|
811 | """
|
---|
812 | self._oDb.execute('SELECT *\n'
|
---|
813 | 'FROM TestResults\n'
|
---|
814 | 'WHERE idTestResult = %s\n',
|
---|
815 | (idTestResult,))
|
---|
816 |
|
---|
817 | aRows = self._oDb.fetchAll()
|
---|
818 | if len(aRows) not in (0, 1):
|
---|
819 | raise TMExceptionBase('Found more than one test result with the same credentials. Database structure is corrupted.')
|
---|
820 | try:
|
---|
821 | return TestResultData().initFromDbRow(aRows[0])
|
---|
822 | except IndexError:
|
---|
823 | return None
|
---|
824 |
|
---|
825 |
|
---|
826 | #
|
---|
827 | # Details view and interface.
|
---|
828 | #
|
---|
829 |
|
---|
830 | def fetchResultTree(self, idTestSet, cMaxDepth = None):
|
---|
831 | """
|
---|
832 | Fetches the result tree for the given test set.
|
---|
833 |
|
---|
834 | Returns a tree of TestResultDataEx nodes.
|
---|
835 | Raises exception on invalid input and database issues.
|
---|
836 | """
|
---|
837 | # Depth first, i.e. just like the XML added them.
|
---|
838 | ## @todo this still isn't performing extremely well, consider optimizations.
|
---|
839 | sQuery = self._oDb.formatBindArgs(
|
---|
840 | 'SELECT TestResults.*,\n'
|
---|
841 | ' TestResultStrTab.sValue,\n'
|
---|
842 | ' EXISTS ( SELECT idTestResultValue\n'
|
---|
843 | ' FROM TestResultValues\n'
|
---|
844 | ' WHERE TestResultValues.idTestResult = TestResults.idTestResult ) AS fHasValues,\n'
|
---|
845 | ' EXISTS ( SELECT idTestResultMsg\n'
|
---|
846 | ' FROM TestResultMsgs\n'
|
---|
847 | ' WHERE TestResultMsgs.idTestResult = TestResults.idTestResult ) AS fHasMsgs,\n'
|
---|
848 | ' EXISTS ( SELECT idTestResultFile\n'
|
---|
849 | ' FROM TestResultFiles\n'
|
---|
850 | ' WHERE TestResultFiles.idTestResult = TestResults.idTestResult ) AS fHasFiles\n'
|
---|
851 | 'FROM TestResults, TestResultStrTab\n'
|
---|
852 | 'WHERE TestResults.idTestSet = %s\n'
|
---|
853 | ' AND TestResults.idStrName = TestResultStrTab.idStr\n'
|
---|
854 | , ( idTestSet, ));
|
---|
855 | if cMaxDepth is not None:
|
---|
856 | sQuery += self._oDb.formatBindArgs(' AND TestResults.iNestingDepth <= %s\n', (cMaxDepth,));
|
---|
857 | sQuery += 'ORDER BY idTestResult ASC\n'
|
---|
858 |
|
---|
859 | self._oDb.execute(sQuery);
|
---|
860 | cRows = self._oDb.getRowCount();
|
---|
861 | if cRows > 65536:
|
---|
862 | raise TMTooManyRows('Too many rows returned for idTestSet=%d: %d' % (idTestSet, cRows,));
|
---|
863 |
|
---|
864 | aaoRows = self._oDb.fetchAll();
|
---|
865 | if len(aaoRows) == 0:
|
---|
866 | raise TMExceptionBase('No test results for idTestSet=%d.' % (idTestSet,));
|
---|
867 |
|
---|
868 | # Set up the root node first.
|
---|
869 | aoRow = aaoRows[0];
|
---|
870 | oRoot = TestResultDataEx().initFromDbRow(aoRow);
|
---|
871 | if oRoot.idTestResultParent is not None:
|
---|
872 | raise self._oDb.integrityException('The root TestResult (#%s) has a parent (#%s)!'
|
---|
873 | % (oRoot.idTestResult, oRoot.idTestResultParent));
|
---|
874 | self._fetchResultTreeNodeExtras(oRoot, aoRow[-3], aoRow[-2], aoRow[-1]);
|
---|
875 |
|
---|
876 | # The chilren (if any).
|
---|
877 | dLookup = { oRoot.idTestResult: oRoot };
|
---|
878 | oParent = oRoot;
|
---|
879 | for iRow in range(1, len(aaoRows)):
|
---|
880 | aoRow = aaoRows[iRow];
|
---|
881 | oCur = TestResultDataEx().initFromDbRow(aoRow);
|
---|
882 | self._fetchResultTreeNodeExtras(oCur, aoRow[-3], aoRow[-2], aoRow[-1]);
|
---|
883 |
|
---|
884 | # Figure out and vet the parent.
|
---|
885 | if oParent.idTestResult != oCur.idTestResultParent:
|
---|
886 | oParent = dLookup.get(oCur.idTestResultParent, None);
|
---|
887 | if oParent is None:
|
---|
888 | raise self._oDb.integrityException('TestResult #%d is orphaned from its parent #%s.'
|
---|
889 | % (oCur.idTestResult, oCur.idTestResultParent,));
|
---|
890 | if oParent.iNestingDepth + 1 != oCur.iNestingDepth:
|
---|
891 | raise self._oDb.integrityException('TestResult #%d has incorrect nesting depth (%d instead of %d)'
|
---|
892 | % (oCur.idTestResult, oCur.iNestingDepth, oParent.iNestingDepth + 1,));
|
---|
893 |
|
---|
894 | # Link it up.
|
---|
895 | oCur.oParent = oParent;
|
---|
896 | oParent.aoChildren.append(oCur);
|
---|
897 | dLookup[oCur.idTestResult] = oCur;
|
---|
898 |
|
---|
899 | return (oRoot, dLookup);
|
---|
900 |
|
---|
901 | def _fetchResultTreeNodeExtras(self, oCurNode, fHasValues, fHasMsgs, fHasFiles):
|
---|
902 | """
|
---|
903 | fetchResultTree worker that fetches values, message and files for the
|
---|
904 | specified node.
|
---|
905 | """
|
---|
906 | assert(oCurNode.aoValues == []);
|
---|
907 | assert(oCurNode.aoMsgs == []);
|
---|
908 | assert(oCurNode.aoFiles == []);
|
---|
909 |
|
---|
910 | if fHasValues:
|
---|
911 | self._oDb.execute('SELECT TestResultValues.*,\n'
|
---|
912 | ' TestResultStrTab.sValue\n'
|
---|
913 | 'FROM TestResultValues, TestResultStrTab\n'
|
---|
914 | 'WHERE TestResultValues.idTestResult = %s\n'
|
---|
915 | ' AND TestResultValues.idStrName = TestResultStrTab.idStr\n'
|
---|
916 | 'ORDER BY idTestResultValue ASC\n'
|
---|
917 | , ( oCurNode.idTestResult, ));
|
---|
918 | for aoRow in self._oDb.fetchAll():
|
---|
919 | oCurNode.aoValues.append(TestResultValueDataEx().initFromDbRow(aoRow));
|
---|
920 |
|
---|
921 | if fHasMsgs:
|
---|
922 | self._oDb.execute('SELECT TestResultMsgs.*,\n'
|
---|
923 | ' TestResultStrTab.sValue\n'
|
---|
924 | 'FROM TestResultMsgs, TestResultStrTab\n'
|
---|
925 | 'WHERE TestResultMsgs.idTestResult = %s\n'
|
---|
926 | ' AND TestResultMsgs.idStrMsg = TestResultStrTab.idStr\n'
|
---|
927 | 'ORDER BY idTestResultMsg ASC\n'
|
---|
928 | , ( oCurNode.idTestResult, ));
|
---|
929 | for aoRow in self._oDb.fetchAll():
|
---|
930 | oCurNode.aoMsgs.append(TestResultMsgDataEx().initFromDbRow(aoRow));
|
---|
931 |
|
---|
932 | if fHasFiles:
|
---|
933 | self._oDb.execute('SELECT TestResultFiles.*,\n'
|
---|
934 | ' StrTabFile.sValue AS sFile,\n'
|
---|
935 | ' StrTabDesc.sValue AS sDescription,\n'
|
---|
936 | ' StrTabKind.sValue AS sKind,\n'
|
---|
937 | ' StrTabMime.sValue AS sMime\n'
|
---|
938 | 'FROM TestResultFiles,\n'
|
---|
939 | ' TestResultStrTab AS StrTabFile,\n'
|
---|
940 | ' TestResultStrTab AS StrTabDesc,\n'
|
---|
941 | ' TestResultStrTab AS StrTabKind,\n'
|
---|
942 | ' TestResultStrTab AS StrTabMime\n'
|
---|
943 | 'WHERE TestResultFiles.idTestResult = %s\n'
|
---|
944 | ' AND TestResultFiles.idStrFile = StrTabFile.idStr\n'
|
---|
945 | ' AND TestResultFiles.idStrDescription = StrTabDesc.idStr\n'
|
---|
946 | ' AND TestResultFiles.idStrKind = StrTabKind.idStr\n'
|
---|
947 | ' AND TestResultFiles.idStrMime = StrTabMime.idStr\n'
|
---|
948 | 'ORDER BY idTestResultFile ASC\n'
|
---|
949 | , ( oCurNode.idTestResult, ));
|
---|
950 | for aoRow in self._oDb.fetchAll():
|
---|
951 | oCurNode.aoFiles.append(TestResultFileDataEx().initFromDbRow(aoRow));
|
---|
952 |
|
---|
953 | return True;
|
---|
954 |
|
---|
955 |
|
---|
956 |
|
---|
957 | #
|
---|
958 | # TestBoxController interface(s).
|
---|
959 | #
|
---|
960 |
|
---|
961 | def _inhumeTestResults(self, aoStack, idTestSet, sError):
|
---|
962 | """
|
---|
963 | The test produces too much output, kill and bury it.
|
---|
964 |
|
---|
965 | Note! We leave the test set open, only the test result records are
|
---|
966 | completed. Thus, _getResultStack will return an empty stack and
|
---|
967 | cause XML processing to fail immediately, while we can still
|
---|
968 | record when it actually completed in the test set the normal way.
|
---|
969 | """
|
---|
970 | self._oDb.dprint('** _inhumeTestResults: idTestSet=%d\n%s' % (idTestSet, self._stringifyStack(aoStack),));
|
---|
971 |
|
---|
972 | #
|
---|
973 | # First add a message.
|
---|
974 | #
|
---|
975 | self._newFailureDetails(aoStack[0].idTestResult, sError, None);
|
---|
976 |
|
---|
977 | #
|
---|
978 | # The complete all open test results.
|
---|
979 | #
|
---|
980 | for oTestResult in aoStack:
|
---|
981 | oTestResult.cErrors += 1;
|
---|
982 | self._completeTestResults(oTestResult, None, TestResultData.ksTestStatus_Failure, oTestResult.cErrors);
|
---|
983 |
|
---|
984 | # A bit of paranoia.
|
---|
985 | self._oDb.execute('UPDATE TestResults\n'
|
---|
986 | 'SET cErrors = cErrors + 1,\n'
|
---|
987 | ' enmStatus = \'failure\'::TestStatus_T,\n'
|
---|
988 | ' tsElapsed = CURRENT_TIMESTAMP - tsCreated\n'
|
---|
989 | 'WHERE idTestSet = %s\n'
|
---|
990 | ' AND enmStatus = \'running\'::TestStatus_T\n'
|
---|
991 | , ( idTestSet, ));
|
---|
992 | self._oDb.commit();
|
---|
993 |
|
---|
994 | return None;
|
---|
995 |
|
---|
996 | def strTabString(self, sString, fCommit = False):
|
---|
997 | """
|
---|
998 | Gets the string table id for the given string, adding it if new.
|
---|
999 |
|
---|
1000 | Note! A copy of this code is also in TestSetLogic.
|
---|
1001 | """
|
---|
1002 | ## @todo move this and make a stored procedure for it.
|
---|
1003 | self._oDb.execute('SELECT idStr\n'
|
---|
1004 | 'FROM TestResultStrTab\n'
|
---|
1005 | 'WHERE sValue = %s'
|
---|
1006 | , (sString,));
|
---|
1007 | if self._oDb.getRowCount() == 0:
|
---|
1008 | self._oDb.execute('INSERT INTO TestResultStrTab (sValue)\n'
|
---|
1009 | 'VALUES (%s)\n'
|
---|
1010 | 'RETURNING idStr\n'
|
---|
1011 | , (sString,));
|
---|
1012 | if fCommit:
|
---|
1013 | self._oDb.commit();
|
---|
1014 | return self._oDb.fetchOne()[0];
|
---|
1015 |
|
---|
1016 | @staticmethod
|
---|
1017 | def _stringifyStack(aoStack):
|
---|
1018 | """Returns a string rep of the stack."""
|
---|
1019 | sRet = '';
|
---|
1020 | for i in range(len(aoStack)):
|
---|
1021 | sRet += 'aoStack[%d]=%s\n' % (i, aoStack[i]);
|
---|
1022 | return sRet;
|
---|
1023 |
|
---|
1024 | def _getResultStack(self, idTestSet):
|
---|
1025 | """
|
---|
1026 | Gets the current stack of result sets.
|
---|
1027 | """
|
---|
1028 | self._oDb.execute('SELECT *\n'
|
---|
1029 | 'FROM TestResults\n'
|
---|
1030 | 'WHERE idTestSet = %s\n'
|
---|
1031 | ' AND enmStatus = \'running\'::TestStatus_T\n'
|
---|
1032 | 'ORDER BY idTestResult DESC'
|
---|
1033 | , ( idTestSet, ));
|
---|
1034 | aoStack = [];
|
---|
1035 | for aoRow in self._oDb.fetchAll():
|
---|
1036 | aoStack.append(TestResultData().initFromDbRow(aoRow));
|
---|
1037 |
|
---|
1038 | for i in range(len(aoStack)):
|
---|
1039 | assert aoStack[i].iNestingDepth == len(aoStack) - i - 1, self._stringifyStack(aoStack);
|
---|
1040 |
|
---|
1041 | return aoStack;
|
---|
1042 |
|
---|
1043 | def _newTestResult(self, idTestResultParent, idTestSet, iNestingDepth, tsCreated, sName, dCounts, fCommit = False):
|
---|
1044 | """
|
---|
1045 | Creates a new test result.
|
---|
1046 | Returns the TestResultData object for the new record.
|
---|
1047 | May raise exception on database error.
|
---|
1048 | """
|
---|
1049 | assert idTestResultParent is not None;
|
---|
1050 | assert idTestResultParent > 1;
|
---|
1051 |
|
---|
1052 | #
|
---|
1053 | # This isn't necessarily very efficient, but it's necessary to prevent
|
---|
1054 | # a wild test or testbox from filling up the database.
|
---|
1055 | #
|
---|
1056 | sCountName = 'cTestResults';
|
---|
1057 | if sCountName not in dCounts:
|
---|
1058 | self._oDb.execute('SELECT COUNT(idTestResult)\n'
|
---|
1059 | 'FROM TestResults\n'
|
---|
1060 | 'WHERE idTestSet = %s\n'
|
---|
1061 | , ( idTestSet,));
|
---|
1062 | dCounts[sCountName] = self._oDb.fetchOne()[0];
|
---|
1063 | dCounts[sCountName] += 1;
|
---|
1064 | if dCounts[sCountName] > config.g_kcMaxTestResultsPerTS:
|
---|
1065 | raise TestResultHangingOffence('Too many sub-tests in total!');
|
---|
1066 |
|
---|
1067 | sCountName = 'cTestResultsIn%d' % (idTestResultParent,);
|
---|
1068 | if sCountName not in dCounts:
|
---|
1069 | self._oDb.execute('SELECT COUNT(idTestResult)\n'
|
---|
1070 | 'FROM TestResults\n'
|
---|
1071 | 'WHERE idTestResultParent = %s\n'
|
---|
1072 | , ( idTestResultParent,));
|
---|
1073 | dCounts[sCountName] = self._oDb.fetchOne()[0];
|
---|
1074 | dCounts[sCountName] += 1;
|
---|
1075 | if dCounts[sCountName] > config.g_kcMaxTestResultsPerTR:
|
---|
1076 | raise TestResultHangingOffence('Too many immediate sub-tests!');
|
---|
1077 |
|
---|
1078 | # This is also a hanging offence.
|
---|
1079 | if iNestingDepth > config.g_kcMaxTestResultDepth:
|
---|
1080 | raise TestResultHangingOffence('To deep sub-test nesting!');
|
---|
1081 |
|
---|
1082 | # Ditto.
|
---|
1083 | if len(sName) > config.g_kcchMaxTestResultName:
|
---|
1084 | raise TestResultHangingOffence('Test name is too long: %d chars - "%s"' % (len(sName), sName));
|
---|
1085 |
|
---|
1086 | #
|
---|
1087 | # Within bounds, do the job.
|
---|
1088 | #
|
---|
1089 | idStrName = self.strTabString(sName, fCommit);
|
---|
1090 | self._oDb.execute('INSERT INTO TestResults (\n'
|
---|
1091 | ' idTestResultParent,\n'
|
---|
1092 | ' idTestSet,\n'
|
---|
1093 | ' tsCreated,\n'
|
---|
1094 | ' idStrName,\n'
|
---|
1095 | ' iNestingDepth )\n'
|
---|
1096 | 'VALUES (%s, %s, TIMESTAMP WITH TIME ZONE %s, %s, %s)\n'
|
---|
1097 | 'RETURNING *\n'
|
---|
1098 | , ( idTestResultParent, idTestSet, tsCreated, idStrName, iNestingDepth) )
|
---|
1099 | oData = TestResultData().initFromDbRow(self._oDb.fetchOne());
|
---|
1100 |
|
---|
1101 | self._oDb.maybeCommit(fCommit);
|
---|
1102 | return oData;
|
---|
1103 |
|
---|
1104 | def _newTestValue(self, idTestResult, idTestSet, sName, lValue, sUnit, dCounts, tsCreated = None, fCommit = False):
|
---|
1105 | """
|
---|
1106 | Creates a test value.
|
---|
1107 | May raise exception on database error.
|
---|
1108 | """
|
---|
1109 |
|
---|
1110 | #
|
---|
1111 | # Bounds checking.
|
---|
1112 | #
|
---|
1113 | sCountName = 'cTestValues';
|
---|
1114 | if sCountName not in dCounts:
|
---|
1115 | self._oDb.execute('SELECT COUNT(idTestResultValue)\n'
|
---|
1116 | 'FROM TestResultValues, TestResults\n'
|
---|
1117 | 'WHERE TestResultValues.idTestResult = TestResults.idTestResult\n'
|
---|
1118 | ' AND TestResults.idTestSet = %s\n'
|
---|
1119 | , ( idTestSet,));
|
---|
1120 | dCounts[sCountName] = self._oDb.fetchOne()[0];
|
---|
1121 | dCounts[sCountName] += 1;
|
---|
1122 | if dCounts[sCountName] > config.g_kcMaxTestValuesPerTS:
|
---|
1123 | raise TestResultHangingOffence('Too many values in total!');
|
---|
1124 |
|
---|
1125 | sCountName = 'cTestValuesIn%d' % (idTestResult,);
|
---|
1126 | if sCountName not in dCounts:
|
---|
1127 | self._oDb.execute('SELECT COUNT(idTestResultValue)\n'
|
---|
1128 | 'FROM TestResultValues\n'
|
---|
1129 | 'WHERE idTestResult = %s\n'
|
---|
1130 | , ( idTestResult,));
|
---|
1131 | dCounts[sCountName] = self._oDb.fetchOne()[0];
|
---|
1132 | dCounts[sCountName] += 1;
|
---|
1133 | if dCounts[sCountName] > config.g_kcMaxTestValuesPerTR:
|
---|
1134 | raise TestResultHangingOffence('Too many immediate values for one test result!');
|
---|
1135 |
|
---|
1136 | if len(sName) > config.g_kcchMaxTestValueName:
|
---|
1137 | raise TestResultHangingOffence('Value name is too long: %d chars - "%s"' % (len(sName), sName));
|
---|
1138 |
|
---|
1139 | #
|
---|
1140 | # Do the job.
|
---|
1141 | #
|
---|
1142 | iUnit = constants.valueunit.g_kdNameToConst.get(sUnit, constants.valueunit.NONE);
|
---|
1143 |
|
---|
1144 | idStrName = self.strTabString(sName, fCommit);
|
---|
1145 | if tsCreated is None:
|
---|
1146 | self._oDb.execute('INSERT INTO TestResultValues (\n'
|
---|
1147 | ' idTestResult,\n'
|
---|
1148 | ' idTestSet,\n'
|
---|
1149 | ' idStrName,\n'
|
---|
1150 | ' lValue,\n'
|
---|
1151 | ' iUnit)\n'
|
---|
1152 | 'VALUES ( %s, %s, %s, %s, %s )\n'
|
---|
1153 | , ( idTestResult, idTestSet, idStrName, lValue, iUnit,) );
|
---|
1154 | else:
|
---|
1155 | self._oDb.execute('INSERT INTO TestResultValues (\n'
|
---|
1156 | ' idTestResult,\n'
|
---|
1157 | ' idTestSet,\n'
|
---|
1158 | ' tsCreated,\n'
|
---|
1159 | ' idStrName,\n'
|
---|
1160 | ' lValue,\n'
|
---|
1161 | ' iUnit)\n'
|
---|
1162 | 'VALUES ( %s, %s, TIMESTAMP WITH TIME ZONE %s, %s, %s, %s )\n'
|
---|
1163 | , ( idTestResult, idTestSet, tsCreated, idStrName, lValue, iUnit,) );
|
---|
1164 | self._oDb.maybeCommit(fCommit);
|
---|
1165 | return True;
|
---|
1166 |
|
---|
1167 | def _newFailureDetails(self, idTestResult, sText, dCounts, tsCreated = None, fCommit = False):
|
---|
1168 | """
|
---|
1169 | Creates a record detailing cause of failure.
|
---|
1170 | May raise exception on database error.
|
---|
1171 | """
|
---|
1172 |
|
---|
1173 | #
|
---|
1174 | # Overflow protection.
|
---|
1175 | #
|
---|
1176 | if dCounts is not None:
|
---|
1177 | sCountName = 'cTestMsgsIn%d' % (idTestResult,);
|
---|
1178 | if sCountName not in dCounts:
|
---|
1179 | self._oDb.execute('SELECT COUNT(idTestResultMsg)\n'
|
---|
1180 | 'FROM TestResultMsgs\n'
|
---|
1181 | 'WHERE idTestResult = %s\n'
|
---|
1182 | , ( idTestResult,));
|
---|
1183 | dCounts[sCountName] = self._oDb.fetchOne()[0];
|
---|
1184 | dCounts[sCountName] += 1;
|
---|
1185 | if dCounts[sCountName] > config.g_kcMaxTestMsgsPerTR:
|
---|
1186 | raise TestResultHangingOffence('Too many messages under for one test result!');
|
---|
1187 |
|
---|
1188 | if len(sText) > config.g_kcchMaxTestMsg:
|
---|
1189 | raise TestResultHangingOffence('Failure details message is too long: %d chars - "%s"' % (len(sText), sText));
|
---|
1190 |
|
---|
1191 | #
|
---|
1192 | # Do the job.
|
---|
1193 | #
|
---|
1194 | idStrMsg = self.strTabString(sText, fCommit);
|
---|
1195 | if tsCreated is None:
|
---|
1196 | self._oDb.execute('INSERT INTO TestResultMsgs (\n'
|
---|
1197 | ' idTestResult,\n'
|
---|
1198 | ' idStrMsg,\n'
|
---|
1199 | ' enmLevel)\n'
|
---|
1200 | 'VALUES ( %s, %s, %s)\n'
|
---|
1201 | , ( idTestResult, idStrMsg, 'failure',) );
|
---|
1202 | else:
|
---|
1203 | self._oDb.execute('INSERT INTO TestResultMsgs (\n'
|
---|
1204 | ' idTestResult,\n'
|
---|
1205 | ' tsCreated,\n'
|
---|
1206 | ' idStrMsg,\n'
|
---|
1207 | ' enmLevel)\n'
|
---|
1208 | 'VALUES ( %s, TIMESTAMP WITH TIME ZONE %s, %s, %s)\n'
|
---|
1209 | , ( idTestResult, tsCreated, idStrMsg, 'failure',) );
|
---|
1210 |
|
---|
1211 | self._oDb.maybeCommit(fCommit);
|
---|
1212 | return True;
|
---|
1213 |
|
---|
1214 |
|
---|
1215 | def _completeTestResults(self, oTestResult, tsDone, enmStatus, cErrors = 0, fCommit = False):
|
---|
1216 | """
|
---|
1217 | Completes a test result. Updates the oTestResult object.
|
---|
1218 | May raise exception on database error.
|
---|
1219 | """
|
---|
1220 | self._oDb.dprint('** _completeTestResults: cErrors=%s tsDone=%s enmStatus=%s oTestResults=\n%s'
|
---|
1221 | % (cErrors, tsDone, enmStatus, oTestResult,));
|
---|
1222 |
|
---|
1223 | #
|
---|
1224 | # Sanity check: No open sub tests (aoStack should make sure about this!).
|
---|
1225 | #
|
---|
1226 | self._oDb.execute('SELECT COUNT(idTestResult)\n'
|
---|
1227 | 'FROM TestResults\n'
|
---|
1228 | 'WHERE idTestResultParent = %s\n'
|
---|
1229 | ' AND enmStatus = %s\n'
|
---|
1230 | , ( oTestResult.idTestResult, TestResultData.ksTestStatus_Running,));
|
---|
1231 | cOpenSubTest = self._oDb.fetchOne()[0];
|
---|
1232 | assert cOpenSubTest == 0, 'cOpenSubTest=%d - %s' % (cOpenSubTest, oTestResult,);
|
---|
1233 | assert oTestResult.enmStatus == TestResultData.ksTestStatus_Running;
|
---|
1234 |
|
---|
1235 | #
|
---|
1236 | # Make sure the reporter isn't lying about successes or error counts.
|
---|
1237 | #
|
---|
1238 | self._oDb.execute('SELECT COALESCE(SUM(cErrors), 0)\n'
|
---|
1239 | 'FROM TestResults\n'
|
---|
1240 | 'WHERE idTestResultParent = %s\n'
|
---|
1241 | , ( oTestResult.idTestResult, ));
|
---|
1242 | cMinErrors = self._oDb.fetchOne()[0] + oTestResult.cErrors;
|
---|
1243 | if cErrors < cMinErrors:
|
---|
1244 | cErrors = cMinErrors;
|
---|
1245 | if cErrors > 0 and enmStatus == TestResultData.ksTestStatus_Success:
|
---|
1246 | enmStatus = TestResultData.ksTestStatus_Failure
|
---|
1247 |
|
---|
1248 | #
|
---|
1249 | # Do the update.
|
---|
1250 | #
|
---|
1251 | if tsDone is None:
|
---|
1252 | self._oDb.execute('UPDATE TestResults\n'
|
---|
1253 | 'SET cErrors = %s,\n'
|
---|
1254 | ' enmStatus = %s,\n'
|
---|
1255 | ' tsElapsed = CURRENT_TIMESTAMP - tsCreated\n'
|
---|
1256 | 'WHERE idTestResult = %s\n'
|
---|
1257 | 'RETURNING tsElapsed'
|
---|
1258 | , ( cErrors, enmStatus, oTestResult.idTestResult,) );
|
---|
1259 | else:
|
---|
1260 | self._oDb.execute('UPDATE TestResults\n'
|
---|
1261 | 'SET cErrors = %s,\n'
|
---|
1262 | ' enmStatus = %s,\n'
|
---|
1263 | ' tsElapsed = TIMESTAMP WITH TIME ZONE %s - tsCreated\n'
|
---|
1264 | 'WHERE idTestResult = %s\n'
|
---|
1265 | 'RETURNING tsElapsed'
|
---|
1266 | , ( cErrors, enmStatus, tsDone, oTestResult.idTestResult,) );
|
---|
1267 |
|
---|
1268 | oTestResult.tsElapsed = self._oDb.fetchOne()[0];
|
---|
1269 | oTestResult.enmStatus = enmStatus;
|
---|
1270 | oTestResult.cErrors = cErrors;
|
---|
1271 |
|
---|
1272 | self._oDb.maybeCommit(fCommit);
|
---|
1273 | return None;
|
---|
1274 |
|
---|
1275 | def _doPopHint(self, aoStack, cStackEntries, dCounts):
|
---|
1276 | """ Executes a PopHint. """
|
---|
1277 | assert cStackEntries >= 0;
|
---|
1278 | while len(aoStack) > cStackEntries:
|
---|
1279 | if aoStack[0].enmStatus == TestResultData.ksTestStatus_Running:
|
---|
1280 | self._newFailureDetails(aoStack[0].idTestResult, 'XML error: Missing </Test>', dCounts);
|
---|
1281 | self._completeTestResults(aoStack[0], tsDone = None, cErrors = 1,
|
---|
1282 | enmStatus = TestResultData.ksTestStatus_Failure, fCommit = True);
|
---|
1283 | aoStack.pop(0);
|
---|
1284 | return True;
|
---|
1285 |
|
---|
1286 |
|
---|
1287 | @staticmethod
|
---|
1288 | def _validateElement(sName, dAttribs, fClosed):
|
---|
1289 | """
|
---|
1290 | Validates an element and its attributes.
|
---|
1291 | """
|
---|
1292 |
|
---|
1293 | #
|
---|
1294 | # Validate attributes by name.
|
---|
1295 | #
|
---|
1296 |
|
---|
1297 | # Validate integer attributes.
|
---|
1298 | for sAttr in [ 'errors', 'testdepth' ]:
|
---|
1299 | if sAttr in dAttribs:
|
---|
1300 | try:
|
---|
1301 | _ = int(dAttribs[sAttr]);
|
---|
1302 | except:
|
---|
1303 | return 'Element %s has an invalid %s attribute value: %s.' % (sName, sAttr, dAttribs[sAttr],);
|
---|
1304 |
|
---|
1305 | # Validate long attributes.
|
---|
1306 | for sAttr in [ 'value', ]:
|
---|
1307 | if sAttr in dAttribs:
|
---|
1308 | try:
|
---|
1309 | _ = long(dAttribs[sAttr]);
|
---|
1310 | except:
|
---|
1311 | return 'Element %s has an invalid %s attribute value: %s.' % (sName, sAttr, dAttribs[sAttr],);
|
---|
1312 |
|
---|
1313 | # Validate string attributes.
|
---|
1314 | for sAttr in [ 'name', 'unit', 'text' ]:
|
---|
1315 | if sAttr in dAttribs and len(dAttribs[sAttr]) == 0:
|
---|
1316 | return 'Element %s has an empty %s attribute value.' % (sName, sAttr,);
|
---|
1317 |
|
---|
1318 | # Validate the timestamp attribute.
|
---|
1319 | if 'timestamp' in dAttribs:
|
---|
1320 | (dAttribs['timestamp'], sError) = ModelDataBase.validateTs(dAttribs['timestamp'], fAllowNull = False);
|
---|
1321 | if sError is not None:
|
---|
1322 | return 'Element %s has an invalid timestamp ("%s"): %s' % (sName, dAttribs['timestamp'], sError,);
|
---|
1323 |
|
---|
1324 |
|
---|
1325 | #
|
---|
1326 | # Check that attributes that are required are present.
|
---|
1327 | # We ignore extra attributes.
|
---|
1328 | #
|
---|
1329 | dElementAttribs = \
|
---|
1330 | {
|
---|
1331 | 'Test': [ 'timestamp', 'name', ],
|
---|
1332 | 'Value': [ 'timestamp', 'name', 'unit', 'value', ],
|
---|
1333 | 'FailureDetails': [ 'timestamp', 'text', ],
|
---|
1334 | 'Passed': [ 'timestamp', ],
|
---|
1335 | 'Skipped': [ 'timestamp', ],
|
---|
1336 | 'Failed': [ 'timestamp', 'errors', ],
|
---|
1337 | 'TimedOut': [ 'timestamp', 'errors', ],
|
---|
1338 | 'End': [ 'timestamp', ],
|
---|
1339 | 'PushHint': [ 'testdepth', ],
|
---|
1340 | 'PopHint': [ 'testdepth', ],
|
---|
1341 | };
|
---|
1342 | if sName not in dElementAttribs:
|
---|
1343 | return 'Unknown element "%s".' % (sName,);
|
---|
1344 | for sAttr in dElementAttribs[sName]:
|
---|
1345 | if sAttr not in dAttribs:
|
---|
1346 | return 'Element %s requires attribute "%s".' % (sName, sAttr);
|
---|
1347 |
|
---|
1348 | #
|
---|
1349 | # Only the Test element can (and must) remain open.
|
---|
1350 | #
|
---|
1351 | if sName == 'Test' and fClosed:
|
---|
1352 | return '<Test/> is not allowed.';
|
---|
1353 | if sName != 'Test' and not fClosed:
|
---|
1354 | return 'All elements except <Test> must be closed.';
|
---|
1355 |
|
---|
1356 | return None;
|
---|
1357 |
|
---|
1358 | @staticmethod
|
---|
1359 | def _parseElement(sElement):
|
---|
1360 | """
|
---|
1361 | Parses an element.
|
---|
1362 |
|
---|
1363 | """
|
---|
1364 | #
|
---|
1365 | # Element level bits.
|
---|
1366 | #
|
---|
1367 | sName = sElement.split()[0];
|
---|
1368 | sElement = sElement[len(sName):];
|
---|
1369 |
|
---|
1370 | fClosed = sElement[-1] == '/';
|
---|
1371 | if fClosed:
|
---|
1372 | sElement = sElement[:-1];
|
---|
1373 |
|
---|
1374 | #
|
---|
1375 | # Attributes.
|
---|
1376 | #
|
---|
1377 | sError = None;
|
---|
1378 | dAttribs = {};
|
---|
1379 | sElement = sElement.strip();
|
---|
1380 | while len(sElement) > 0:
|
---|
1381 | # Extract attribute name.
|
---|
1382 | off = sElement.find('=');
|
---|
1383 | if off < 0 or not sElement[:off].isalnum():
|
---|
1384 | sError = 'Attributes shall have alpha numberical names and have values.';
|
---|
1385 | break;
|
---|
1386 | sAttr = sElement[:off];
|
---|
1387 |
|
---|
1388 | # Extract attribute value.
|
---|
1389 | if off + 2 >= len(sElement) or sElement[off + 1] != '"':
|
---|
1390 | sError = 'Attribute (%s) value is missing or not in double quotes.' % (sAttr,);
|
---|
1391 | break;
|
---|
1392 | off += 2;
|
---|
1393 | offEndQuote = sElement.find('"', off);
|
---|
1394 | if offEndQuote < 0:
|
---|
1395 | sError = 'Attribute (%s) value is missing end quotation mark.' % (sAttr,);
|
---|
1396 | break;
|
---|
1397 | sValue = sElement[off:offEndQuote];
|
---|
1398 |
|
---|
1399 | # Check for duplicates.
|
---|
1400 | if sAttr in dAttribs:
|
---|
1401 | sError = 'Attribute "%s" appears more than once.' % (sAttr,);
|
---|
1402 | break;
|
---|
1403 |
|
---|
1404 | # Unescape the value.
|
---|
1405 | sValue = sValue.replace('<', '<');
|
---|
1406 | sValue = sValue.replace('>', '>');
|
---|
1407 | sValue = sValue.replace(''', '\'');
|
---|
1408 | sValue = sValue.replace('"', '"');
|
---|
1409 | sValue = sValue.replace('
', '\n');
|
---|
1410 | sValue = sValue.replace('
', '\r');
|
---|
1411 | sValue = sValue.replace('&', '&'); # last
|
---|
1412 |
|
---|
1413 | # Done.
|
---|
1414 | dAttribs[sAttr] = sValue;
|
---|
1415 |
|
---|
1416 | # advance
|
---|
1417 | sElement = sElement[offEndQuote + 1:];
|
---|
1418 | sElement = sElement.lstrip();
|
---|
1419 |
|
---|
1420 | #
|
---|
1421 | # Validate the element before we return.
|
---|
1422 | #
|
---|
1423 | if sError is None:
|
---|
1424 | sError = TestResultLogic._validateElement(sName, dAttribs, fClosed);
|
---|
1425 |
|
---|
1426 | return (sName, dAttribs, sError)
|
---|
1427 |
|
---|
1428 | def _handleElement(self, sName, dAttribs, idTestSet, aoStack, aaiHints, dCounts):
|
---|
1429 | """
|
---|
1430 | Worker for processXmlStream that handles one element.
|
---|
1431 |
|
---|
1432 | Returns None on success, error string on bad XML or similar.
|
---|
1433 | Raises exception on hanging offence and on database error.
|
---|
1434 | """
|
---|
1435 | if sName == 'Test':
|
---|
1436 | iNestingDepth = aoStack[0].iNestingDepth + 1 if len(aoStack) > 0 else 0;
|
---|
1437 | aoStack.insert(0, self._newTestResult(idTestResultParent = aoStack[0].idTestResult, idTestSet = idTestSet,
|
---|
1438 | tsCreated = dAttribs['timestamp'], sName = dAttribs['name'],
|
---|
1439 | iNestingDepth = iNestingDepth, dCounts = dCounts, fCommit = True) );
|
---|
1440 |
|
---|
1441 | elif sName == 'Value':
|
---|
1442 | self._newTestValue(idTestResult = aoStack[0].idTestResult, idTestSet = idTestSet, tsCreated = dAttribs['timestamp'],
|
---|
1443 | sName = dAttribs['name'], sUnit = dAttribs['unit'], lValue = long(dAttribs['value']),
|
---|
1444 | dCounts = dCounts, fCommit = True);
|
---|
1445 |
|
---|
1446 | elif sName == 'FailureDetails':
|
---|
1447 | self._newFailureDetails(idTestResult = aoStack[0].idTestResult, tsCreated = dAttribs['timestamp'],
|
---|
1448 | sText = dAttribs['text'], dCounts = dCounts, fCommit = True);
|
---|
1449 |
|
---|
1450 | elif sName == 'Passed':
|
---|
1451 | self._completeTestResults(aoStack[0], tsDone = dAttribs['timestamp'],
|
---|
1452 | enmStatus = TestResultData.ksTestStatus_Success, fCommit = True);
|
---|
1453 |
|
---|
1454 | elif sName == 'Skipped':
|
---|
1455 | self._completeTestResults(aoStack[0], tsDone = dAttribs['timestamp'],
|
---|
1456 | enmStatus = TestResultData.ksTestStatus_Skipped, fCommit = True);
|
---|
1457 |
|
---|
1458 | elif sName == 'Failed':
|
---|
1459 | self._completeTestResults(aoStack[0], tsDone = dAttribs['timestamp'], cErrors = int(dAttribs['errors']),
|
---|
1460 | enmStatus = TestResultData.ksTestStatus_Failure, fCommit = True);
|
---|
1461 |
|
---|
1462 | elif sName == 'TimedOut':
|
---|
1463 | self._completeTestResults(aoStack[0], tsDone = dAttribs['timestamp'], cErrors = int(dAttribs['errors']),
|
---|
1464 | enmStatus = TestResultData.ksTestStatus_TimedOut, fCommit = True);
|
---|
1465 |
|
---|
1466 | elif sName == 'End':
|
---|
1467 | self._completeTestResults(aoStack[0], tsDone = dAttribs['timestamp'],
|
---|
1468 | cErrors = int(dAttribs.get('errors', '1')),
|
---|
1469 | enmStatus = TestResultData.ksTestStatus_Success, fCommit = True);
|
---|
1470 |
|
---|
1471 | elif sName == 'PushHint':
|
---|
1472 | if len(aaiHints) > 1:
|
---|
1473 | return 'PushHint cannot be nested.'
|
---|
1474 |
|
---|
1475 | aaiHints.insert(0, [len(aoStack), int(dAttribs['testdepth'])]);
|
---|
1476 |
|
---|
1477 | elif sName == 'PopHint':
|
---|
1478 | if len(aaiHints) < 1:
|
---|
1479 | return 'No hint to pop.'
|
---|
1480 |
|
---|
1481 | iDesiredTestDepth = int(dAttribs['testdepth']);
|
---|
1482 | cStackEntries, iTestDepth = aaiHints.pop(0);
|
---|
1483 | self._doPopHint(aoStack, cStackEntries, dCounts); # Fake the necessary '<End/></Test>' tags.
|
---|
1484 | if iDesiredTestDepth != iTestDepth:
|
---|
1485 | return 'PopHint tag has different testdepth: %d, on stack %d.' % (iDesiredTestDepth, iTestDepth);
|
---|
1486 | else:
|
---|
1487 | return 'Unexpected element "%s".' % (sName,);
|
---|
1488 | return None;
|
---|
1489 |
|
---|
1490 |
|
---|
1491 | def processXmlStream(self, sXml, idTestSet):
|
---|
1492 | """
|
---|
1493 | Processes the "XML" stream section given in sXml.
|
---|
1494 |
|
---|
1495 | The sXml isn't a complete XML document, even should we save up all sXml
|
---|
1496 | for a given set, they may not form a complete and well formed XML
|
---|
1497 | document since the test may be aborted, abend or simply be buggy. We
|
---|
1498 | therefore do our own parsing and treat the XML tags as commands more
|
---|
1499 | than anything else.
|
---|
1500 |
|
---|
1501 | Returns (sError, fUnforgivable), where sError is None on success.
|
---|
1502 | May raise database exception.
|
---|
1503 | """
|
---|
1504 | aoStack = self._getResultStack(idTestSet); # [0] == top; [-1] == bottom.
|
---|
1505 | if len(aoStack) == 0:
|
---|
1506 | return ('No open results', True);
|
---|
1507 | self._oDb.dprint('** processXmlStream len(aoStack)=%s' % (len(aoStack),));
|
---|
1508 | #self._oDb.dprint('processXmlStream: %s' % (self._stringifyStack(aoStack),));
|
---|
1509 | #self._oDb.dprint('processXmlStream: sXml=%s' % (sXml,));
|
---|
1510 |
|
---|
1511 | dCounts = {};
|
---|
1512 | aaiHints = [];
|
---|
1513 | sError = None;
|
---|
1514 |
|
---|
1515 | fExpectCloseTest = False;
|
---|
1516 | sXml = sXml.strip();
|
---|
1517 | while len(sXml) > 0:
|
---|
1518 | if sXml.startswith('</Test>'): # Only closing tag.
|
---|
1519 | offNext = len('</Test>');
|
---|
1520 | if len(aoStack) <= 1:
|
---|
1521 | sError = 'Trying to close the top test results.'
|
---|
1522 | break;
|
---|
1523 | # ASSUMES that we've just seen an <End/>, <Passed/>, <Failed/>,
|
---|
1524 | # <TimedOut/> or <Skipped/> tag earlier in this call!
|
---|
1525 | if aoStack[0].enmStatus == TestResultData.ksTestStatus_Running or not fExpectCloseTest:
|
---|
1526 | sError = 'Missing <End/>, <Passed/>, <Failed/>, <TimedOut/> or <Skipped/> tag.';
|
---|
1527 | break;
|
---|
1528 | aoStack.pop(0);
|
---|
1529 | fExpectCloseTest = False;
|
---|
1530 |
|
---|
1531 | elif fExpectCloseTest:
|
---|
1532 | sError = 'Expected </Test>.'
|
---|
1533 | break;
|
---|
1534 |
|
---|
1535 | elif sXml.startswith('<?xml '): # Ignore (included files).
|
---|
1536 | offNext = sXml.find('?>');
|
---|
1537 | if offNext < 0:
|
---|
1538 | sError = 'Unterminated <?xml ?> element.';
|
---|
1539 | break;
|
---|
1540 | offNext += 2;
|
---|
1541 |
|
---|
1542 | elif sXml[0] == '<':
|
---|
1543 | # Parse and check the tag.
|
---|
1544 | if not sXml[1].isalpha():
|
---|
1545 | sError = 'Malformed element.';
|
---|
1546 | break;
|
---|
1547 | offNext = sXml.find('>')
|
---|
1548 | if offNext < 0:
|
---|
1549 | sError = 'Unterminated element.';
|
---|
1550 | break;
|
---|
1551 | (sName, dAttribs, sError) = self._parseElement(sXml[1:offNext]);
|
---|
1552 | offNext += 1;
|
---|
1553 | if sError is not None:
|
---|
1554 | break;
|
---|
1555 |
|
---|
1556 | # Handle it.
|
---|
1557 | try:
|
---|
1558 | sError = self._handleElement(sName, dAttribs, idTestSet, aoStack, aaiHints, dCounts);
|
---|
1559 | except TestResultHangingOffence as oXcpt:
|
---|
1560 | self._inhumeTestResults(aoStack, idTestSet, str(oXcpt));
|
---|
1561 | return (str(oXcpt), True);
|
---|
1562 |
|
---|
1563 |
|
---|
1564 | fExpectCloseTest = sName in [ 'End', 'Passed', 'Failed', 'TimedOut', 'Skipped', ];
|
---|
1565 | else:
|
---|
1566 | sError = 'Unexpected content.';
|
---|
1567 | break;
|
---|
1568 |
|
---|
1569 | # Advance.
|
---|
1570 | sXml = sXml[offNext:];
|
---|
1571 | sXml = sXml.lstrip();
|
---|
1572 |
|
---|
1573 | #
|
---|
1574 | # Post processing checks.
|
---|
1575 | #
|
---|
1576 | if sError is None and fExpectCloseTest:
|
---|
1577 | sError = 'Expected </Test> before the end of the XML section.'
|
---|
1578 | elif sError is None and len(aaiHints) > 0:
|
---|
1579 | sError = 'Expected </PopHint> before the end of the XML section.'
|
---|
1580 | if len(aaiHints) > 0:
|
---|
1581 | self._doPopHint(aoStack, aaiHints[-1][0], dCounts);
|
---|
1582 |
|
---|
1583 | #
|
---|
1584 | # Log the error.
|
---|
1585 | #
|
---|
1586 | if sError is not None:
|
---|
1587 | SystemLogLogic(self._oDb).addEntry(SystemLogData.ksEvent_XmlResultMalformed,
|
---|
1588 | 'idTestSet=%s idTestResult=%s XML="%s" %s'
|
---|
1589 | % ( idTestSet,
|
---|
1590 | aoStack[0].idTestResult if len(aoStack) > 0 else -1,
|
---|
1591 | sXml[:30 if len(sXml) >= 30 else len(sXml)],
|
---|
1592 | sError, ),
|
---|
1593 | cHoursRepeat = 6, fCommit = True);
|
---|
1594 | return (sError, False);
|
---|
1595 |
|
---|
1596 |
|
---|
1597 | #
|
---|
1598 | # Unit testing.
|
---|
1599 | #
|
---|
1600 |
|
---|
1601 | # pylint: disable=C0111
|
---|
1602 | class TestResultDataTestCase(ModelDataBaseTestCase):
|
---|
1603 | def setUp(self):
|
---|
1604 | self.aoSamples = [TestResultData(),];
|
---|
1605 |
|
---|
1606 | class TestResultValueDataTestCase(ModelDataBaseTestCase):
|
---|
1607 | def setUp(self):
|
---|
1608 | self.aoSamples = [TestResultValueData(),];
|
---|
1609 |
|
---|
1610 | if __name__ == '__main__':
|
---|
1611 | unittest.main();
|
---|
1612 | # not reached.
|
---|
1613 |
|
---|